index_html.rs (191703B)
1 pub(super) const INDEX_HTML: &str = concat!( 2 r#"<!doctype html> 3 <html lang="en"> 4 <head> 5 <meta charset="utf-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1"> 7 <link rel="icon" href="/favicon.ico" sizes="any"> 8 <title>iuna</title> 9 <style> 10 [x-cloak] { display: none !important; } 11 .visually-hidden { position: absolute !important; width: 1px !important; height: 1px !important; overflow: hidden !important; clip: rect(0 0 0 0) !important; clip-path: inset(50%) !important; white-space: nowrap !important; } 12 :root { 13 color-scheme: dark; 14 font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; 15 background: #0f1012; 16 color: #e8edf0; 17 } 18 * { box-sizing: border-box; } 19 html { width: 100%; max-width: 100%; overflow-x: hidden; } 20 body { margin: 0; min-height: 100vh; max-width: 100%; overflow-x: hidden; background: #0f1012; color: #e8edf0; } 21 .app-shell { width: 100%; max-width: 100%; min-height: 100vh; display: block; overflow-x: hidden; } 22 .sidebar { position: fixed; z-index: 5; inset: 0 auto 0 0; width: 84px; height: 100vh; display: flex; flex-direction: column; align-items: center; gap: 20px; padding: 16px 10px; background: #15171a; border-right: 1px solid #262b2f; } 23 .brand-mark { position: relative; width: 64px; min-height: 58px; display: grid; place-items: center; overflow: hidden; border: 1px solid transparent; border-radius: 8px; padding: 7px 4px; background: transparent; color: #d5f55f; user-select: none; cursor: pointer; } 24 .brand-mark::after { content: ""; position: absolute; inset: -40% -70%; background: linear-gradient(100deg, transparent 42%, rgba(255, 255, 255, .34) 50%, transparent 58%); transform: translateX(-58%) rotate(8deg); opacity: 0; pointer-events: none; } 25 .brand-mark svg { position: relative; z-index: 1; width: 24px; height: 24px; display: block; } 26 .brand-mark .mark-loop { fill: none; stroke: currentColor; stroke-width: 4.2; stroke-linecap: round; stroke-linejoin: round; } 27 .brand-mark .mark-dot { fill: currentColor; } 28 .brand-mark:hover, .brand-mark.active { border-color: #3b4448; background: #202328; } 29 .brand-mark:hover::after { animation: mark-sheen .72s ease both; } 30 @keyframes mark-sheen { from { opacity: 0; transform: translateX(-58%) rotate(8deg); } 32% { opacity: 1; } to { opacity: 0; transform: translateX(58%) rotate(8deg); } } 31 .side-nav { display: grid; gap: 10px; width: 100%; } 32 .nav-button { width: 64px; min-height: 58px; display: grid; place-items: center; gap: 4px; border: 1px solid transparent; border-radius: 8px; padding: 7px 4px; background: transparent; color: #9fa8ad; } 33 .nav-button svg { width: 21px; height: 21px; stroke: currentColor; stroke-width: 2; fill: none; } 34 .nav-button svg.chain-icon { stroke-width: 1.35; } 35 .nav-button span { font-size: 11px; font-weight: 800; } 36 .nav-button:hover, .nav-button.active { background: #202328; border-color: #3b4448; color: #d5f55f; } 37 .sidebar-bottom-actions { width: 100%; display: grid; gap: 6px; margin-top: auto; } 38 .discord-button { width: 64px; height: 34px; min-height: 34px; display: grid; place-items: center; gap: 2px; border: 1px solid rgba(88, 101, 242, .72); border-radius: 8px; padding: 5px 4px; background: rgba(88, 101, 242, .16); color: #eef1ff; text-align: center; text-decoration: none; } 39 .discord-button svg { width: 18px; height: 18px; fill: currentColor; } 40 .discord-button span { display: none; font-size: 10px; font-weight: 800; } 41 .discord-button:hover { border-color: #8e99ff; background: rgba(88, 101, 242, .28); color: #fff; } 42 .settings-button { width: 64px; min-height: 54px; display: grid; place-items: center; gap: 4px; border: 1px solid transparent; border-radius: 8px; padding: 7px 4px; color: #9fa8ad; background: transparent; text-align: center; } 43 .settings-button svg { width: 23px; height: 23px; stroke: currentColor; stroke-width: 1.9; fill: none; } 44 .settings-button span { display: none; font-size: 10px; font-weight: 800; } 45 .settings-button:hover, .settings-button.active { background: #202328; border-color: #3b4448; color: #d5f55f; } 46 .brand-mark, .nav-button, .discord-button, .settings-button { transition: filter .1s ease, background-color .14s ease, border-color .14s ease, color .14s ease; } 47 .brand-mark:active, .nav-button:active, .discord-button:active, .settings-button:active { filter: brightness(1.14); } 48 .version-panel { width: 64px; display: grid; gap: 4px; justify-items: center; border: 1px solid transparent; border-radius: 8px; padding: 7px 4px; color: #7f888e; background: transparent; font-size: 10px; font-weight: 850; text-align: center; } 49 .version-panel.update { border-color: #566d25; color: #d5f55f; background: #1c2516; cursor: pointer; } 50 .version-panel.checking { color: #a8b2b8; } 51 .version-panel.failed { color: #ffb1a8; } 52 .version-dot { width: 6px; height: 6px; border-radius: 999px; background: #3a4248; } 53 .version-panel.update .version-dot { background: #d5f55f; box-shadow: 0 0 0 3px rgba(213, 245, 95, .12); } 54 .version-panel.failed .version-dot { background: #ff8f82; } 55 .version-label { line-height: 1; } 56 .version-update { color: #d5f55f; font-size: 9px; line-height: 1; text-transform: uppercase; } 57 .content { width: 100%; min-width: 0; min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; overflow-x: hidden; padding: 22px 24px 48px 108px; } 58 main { width: 100%; } 59 main > section { width: 100%; } 60 header { display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; padding: 0 0 18px; } 61 .header-actions { display: flex; gap: 10px; align-items: center; } 62 .basic-status-row { display: inline-flex; gap: 8px; align-items: center; margin-top: 5px; } 63 .basic-status { display: inline-flex; gap: 7px; align-items: center; border: 1px solid transparent; border-radius: 999px; padding: 3px 7px; color: #9eb3bc; font-size: 12px; font-weight: 800; } 64 .basic-status::before { content: ""; width: 6px; height: 6px; flex: 0 0 auto; border-radius: 999px; background: #7f888e; } 65 .basic-status.healthy { color: #d5f55f; } 66 .basic-status.healthy::before { background: #d5f55f; box-shadow: 0 0 12px rgba(213, 245, 95, .45); } 67 .basic-status.syncing { color: #ffd070; } 68 .basic-status.syncing::before { background: #ffd070; } 69 .basic-status.isolated, .basic-status.stale, .basic-status.banned, .basic-status.error { border-color: #6a332c; background: #261817; color: #ffb8ad; font-weight: 900; } 70 .basic-status.isolated::before, .basic-status.stale::before, .basic-status.banned::before, .basic-status.error::before { background: #ff7668; box-shadow: 0 0 12px rgba(255, 118, 104, .38); } 71 .basic-status-detail { padding: 3px 7px; border-color: #3a4248; background: #202328; color: #9fa8ad; font-size: 11px; } 72 .basic-status-detail:hover { border-color: #d5f55f; color: #d5f55f; } 73 .lock-button { padding: 5px 8px; border-color: #3a4248; background: #202328; color: #9fa8ad; font-size: 12px; } 74 .lock-button:hover { border-color: #d5f55f; color: #d5f55f; } 75 h1 { margin: 0 0 4px; font-size: 28px; } 76 h2 { margin: 0 0 12px; font-size: 18px; } 77 h3 { margin: 0 0 10px; font-size: 15px; } 78 button { border: 1px solid #3a4248; border-radius: 6px; padding: 8px 11px; font: inherit; font-weight: 700; background: #191c20; color: #e8edf0; cursor: pointer; } 79 button:hover { border-color: #d5f55f; color: #d5f55f; } 80 button.primary { background: #d5f55f; border-color: #d5f55f; color: #15171a; } 81 button.primary:hover { background: #e4ff83; color: #15171a; } 82 button.subtle { background: transparent; } 83 button.danger { border-color: #8f3730; background: #341918; color: #ffb1a8; } 84 button.danger:hover { border-color: #ff7668; background: #451c1a; color: #ffd8d3; } 85 button:disabled { cursor: default; opacity: .5; } 86 .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; } 87 .metric, .panel { background: #181b1f; border: 1px solid #2a3035; border-radius: 8px; padding: 13px; } 88 .metric .label { color: #8d989f; font-size: 11px; text-transform: uppercase; } 89 .metric .value { margin-top: 7px; font-weight: 850; overflow-wrap: anywhere; } 90 .panel { margin-bottom: 12px; } 91 .dashboard-section { min-height: 0; flex: 1; display: grid; place-items: center; } 92 .dashboard-grid { width: 100%; display: grid; grid-template-columns: repeat(5, minmax(0, 156px)); justify-content: center; align-content: center; gap: 10px; } 93 .dashboard-card { aspect-ratio: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 13px; border: 1px solid #2a3035; border-radius: 9px; padding: 15px; background: #181b1f; text-align: center; } 94 .dashboard-card-icon { width: 52px; height: 52px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 10px; background: #111316; color: #9fa8ad; } 95 .dashboard-card-icon svg { width: 31px; height: 31px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; } 96 .dashboard-card.good .dashboard-card-icon { background: #1c2516; color: #d5f55f; } 97 .dashboard-card.warning .dashboard-card-icon { background: #292315; color: #ffd070; } 98 .dashboard-card.bad .dashboard-card-icon { background: #2a1717; color: #ff8f82; } 99 .dashboard-card-copy { min-width: 0; width: 100%; display: grid; justify-items: center; } 100 .dashboard-card-label { margin-bottom: 6px; color: #8d989f; font-size: 10px; font-weight: 850; text-transform: uppercase; } 101 .dashboard-card-value { color: #d5f55f; font-size: 14px; font-weight: 850; font-variant-numeric: tabular-nums; line-height: 1.25; overflow-wrap: anywhere; } 102 .dashboard-card.balance { padding-inline: 10px; } 103 .dashboard-card.balance .dashboard-card-value, .dashboard-card.burning .dashboard-status { font-size: clamp(10px, 1.25vw, 13px); letter-spacing: -.015em; white-space: nowrap; } 104 .dashboard-card.burning .dashboard-status { width: 100%; text-align: center; } 105 .dashboard-status { color: #8d989f; font-size: 14px; font-weight: 850; font-variant-numeric: tabular-nums; line-height: 1.25; overflow-wrap: anywhere; } 106 .dashboard-card.good .dashboard-status { color: #d5f55f; } 107 .dashboard-card.warning .dashboard-status { color: #ffd070; } 108 .dashboard-card.bad .dashboard-status { color: #ff9d91; } 109 .split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, .72fr); gap: 12px; } 110 form { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; } 111 label { display: grid; gap: 5px; color: #a8b2b8; font-size: 13px; } 112 input, textarea { min-width: 180px; border: 1px solid #3a444b; border-radius: 6px; padding: 9px 10px; font: inherit; background: #101215; color: #edf2f5; } 113 textarea { min-height: 118px; resize: vertical; line-height: 1.45; } 114 input:focus, textarea:focus { outline: 2px solid #d5f55f; outline-offset: 1px; } 115 table { width: 100%; border-collapse: collapse; font-size: 13px; } 116 th, td { text-align: left; border-bottom: 1px solid #2a3035; padding: 8px; vertical-align: top; } 117 th { color: #8d989f; font-size: 11px; text-transform: uppercase; } 118 code { overflow-wrap: anywhere; color: #c7f5ea; } 119 .table-wrap { max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; } 120 .muted { color: #8d989f; } 121 .flash { position: fixed; top: 18px; right: 18px; z-index: 80; width: min(420px, calc(100vw - 36px)); border-radius: 6px; padding: 10px 12px; border: 1px solid; font-weight: 700; box-shadow: 0 18px 48px rgba(0, 0, 0, .38); } 122 .flash.success { color: #d5f55f; background: #1c2516; border-color: #566d25; } 123 .flash.error { color: #ffb1a8; background: #2a1717; border-color: #713434; } 124 .persistent-banner { border: 1px solid #566d25; border-radius: 8px; padding: 10px 12px; margin: -4px 0 16px; color: #d5f55f; background: #1c2516; font-weight: 800; } 125 .ok { color: #d5f55f; } 126 .page-title { margin-bottom: 16px; } 127 .setup-overlay { position: fixed; inset: 0; z-index: 30; display: grid; place-items: center; padding: 22px; background: rgba(8, 9, 10, .72); backdrop-filter: blur(8px); } 128 .transaction-overlay { z-index: 40; } 129 .sync-overlay { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 22px; background: #0f1012; } 130 .sync-screen { width: min(460px, 100%); display: grid; justify-items: center; gap: 18px; text-align: center; } 131 .sync-mark { width: 54px; height: 54px; display: grid; place-items: center; border: 1px solid #566d25; border-radius: 14px; background: #1c2516; box-shadow: 0 18px 48px rgba(0, 0, 0, .32); } 132 .sync-spinner { width: 25px; height: 25px; border: 3px solid rgba(213, 245, 95, .2); border-top-color: #d5f55f; border-radius: 999px; animation: sync-spin .9s linear infinite; } 133 .sync-screen h1 { margin: 0; font-size: clamp(25px, 5vw, 36px); } 134 .sync-copy { margin: -8px 0 0; color: #9fa8ad; line-height: 1.5; } 135 .sync-progress { width: 100%; height: 9px; overflow: hidden; border: 1px solid #30373b; border-radius: 999px; background: #191c1f; } 136 .sync-progress-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #8de9cd, #d5f55f); transition: width .35s ease; } 137 .sync-progress-label { color: #d5f55f; font-size: 14px; font-weight: 850; font-variant-numeric: tabular-nums; } 138 @keyframes sync-spin { to { transform: rotate(360deg); } } 139 @media (prefers-reduced-motion: reduce) { .sync-spinner { animation-duration: 1.8s; } .sync-progress-fill, .brand-mark, .nav-button, .discord-button, .settings-button { transition: none; } .brand-mark:hover::after { animation: none; } } 140 .setup-modal { width: min(980px, 100%); max-height: calc(100vh - 44px); overflow: auto; border: 1px solid #3b4448; border-radius: 8px; padding: 18px; background: #181b1f; box-shadow: 0 24px 80px rgba(0, 0, 0, .42); } 141 .setup-modal-head { display: grid; gap: 5px; margin-bottom: 16px; } 142 .setup-modal-head h2 { margin: 0; font-size: 24px; } 143 .setup-welcome { color: #d5f55f; font-size: 12px; font-weight: 900; text-transform: uppercase; } 144 .setup-copy { max-width: 620px; color: #a8b2b8; line-height: 1.45; } 145 .setup-feedback { border: 1px solid; border-radius: 8px; padding: 10px 12px; margin-bottom: 14px; font-weight: 800; } 146 .setup-feedback.success { color: #d5f55f; background: #1c2516; border-color: #566d25; } 147 .setup-feedback.error { color: #ffb1a8; background: #2a1717; border-color: #713434; } 148 .setup-grid { width: 100%; display: grid; grid-template-columns: minmax(0, .9fr) minmax(320px, .7fr); gap: 12px; align-items: start; } 149 .setup-section { border: 1px solid #2f363c; border-radius: 8px; padding: 13px; background: #111316; } 150 .setup-node-mode, .setup-network, .setup-wallet-section { grid-column: 1 / -1; } 151 .segmented.setup-mode-picker { width: 100%; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); } 152 .setup-mode-picker button { min-width: 0; min-height: 38px; white-space: normal; } 153 .setup-network-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; align-items: end; } 154 .setup-network-copy { margin-top: 8px; color: #a8b2b8; line-height: 1.45; } 155 .setup-network-link { color: #d5f55f; font-size: 12px; font-weight: 900; text-decoration: none; } 156 .setup-network-link:hover { text-decoration: underline; } 157 .setup-field { display: grid; gap: 6px; } 158 .setup-field-label { color: #8d989f; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0; } 159 .setup-address-box { display: flex; justify-content: space-between; gap: 10px; align-items: center; } 160 .setup-address-box code { min-width: 0; } 161 .setup-address-box button { flex: 0 0 auto; } 162 .setup-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 14px; } 163 .segmented { display: inline-flex; gap: 4px; padding: 4px; border: 1px solid #2f363c; border-radius: 8px; background: #181b1f; } 164 .segmented button { border-color: transparent; background: transparent; color: #9fa8ad; } 165 .segmented button.active { background: #d5f55f; color: #15171a; } 166 .seed-panel { display: grid; gap: 12px; } 167 .seed-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } 168 .seed-word { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 7px; align-items: center; border: 1px solid #2f363c; border-radius: 6px; padding: 7px 8px; background: #181b1f; } 169 .seed-word .index { color: #8d989f; font-size: 11px; font-weight: 800; } 170 .seed-word .word { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-weight: 800; color: #c7f5ea; } 171 .verify-grid { display: grid; gap: 8px; } 172 .setup-status { border: 1px solid #566d25; border-radius: 8px; padding: 10px; background: #1c2516; color: #d5f55f; font-weight: 800; } 173 .auth-form { width: min(420px, 100%); display: grid; gap: 10px; } 174 .auth-form form { display: grid; gap: 10px; align-items: stretch; } 175 .auth-form input { width: 100%; } 176 .settings-grid { width: min(760px, 100%); display: grid; gap: 12px; } 177 .settings-mode-row { display: flex; justify-content: space-between; gap: 14px; align-items: center; } 178 .settings-mode-copy { min-width: 0; display: grid; gap: 4px; } 179 .settings-mode-title { color: #e8edf0; font-size: 15px; font-weight: 850; } 180 .settings-form { display: grid; gap: 10px; align-items: stretch; } 181 .public-p2p-form { margin-top: 14px; } 182 .settings-form label, .settings-form input { width: 100%; } 183 .danger-panel { border-color: #6a332c; background: #201313; } 184 .danger-panel h3, .danger-title { color: #ffb1a8; } 185 .danger-copy { color: #d69a92; line-height: 1.45; } 186 .danger-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 12px; } 187 .metrics-shell { display: grid; gap: 12px; } 188 .metrics-head { display: flex; justify-content: flex-end; gap: 12px; align-items: center; } 189 .metrics-range { flex: 0 0 auto; } 190 .metrics-range button { padding: 5px 9px; font-size: 12px; white-space: nowrap; } 191 .metrics-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; } 192 .metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 430px), 1fr)); gap: 12px; } 193 .metric-chart-card { display: grid; gap: 10px; min-width: 0; border: 1px solid #2a3035; border-radius: 8px; padding: 12px; background: #181b1f; } 194 .metric-chart-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; } 195 .metric-chart-title { margin: 0; color: #e8edf0; font-size: 14px; font-weight: 850; } 196 .metric-chart-value { color: #d5f55f; font-size: 13px; font-weight: 850; font-variant-numeric: tabular-nums; } 197 .metric-chart-frame { display: grid; grid-template-columns: 50px minmax(0, 1fr); grid-template-rows: 156px 18px; column-gap: 6px; row-gap: 4px; min-width: 0; } 198 .metric-chart-plot { position: relative; min-width: 0; } 199 .metric-chart-svg { width: 100%; height: 156px; display: block; border: 1px solid #2f363c; border-radius: 8px; background: #111316; } 200 .metric-chart-gridline { stroke: #3a4248; stroke-width: .8; stroke-dasharray: 3 7; opacity: .58; } 201 .metric-chart-axis { stroke: #3a4248; stroke-width: 1.2; } 202 .metric-chart-axis-label { color: #8d989f; font-size: 10px; font-weight: 750; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 203 .metric-chart-y-axis { position: relative; min-width: 0; } 204 .metric-chart-y-axis .metric-chart-axis-label { position: absolute; right: 0; transform: translateY(-50%); max-width: 100%; } 205 .metric-chart-x-axis { position: relative; grid-column: 2; min-width: 0; overflow: visible; } 206 .metric-chart-x-axis .metric-chart-axis-label { position: absolute; top: 0; transform: translateX(-50%); } 207 .metric-chart-line { fill: none; stroke: #d5f55f; stroke-width: 2.2; stroke-linejoin: round; stroke-linecap: round; } 208 .metric-chart-hover-point { position: absolute; width: 8px; height: 8px; border-radius: 50%; background: #d5f55f; pointer-events: none; transform: translate(-50%, -50%); box-shadow: 0 0 0 4px rgba(213, 245, 95, .18); } 209 .metric-chart-tooltip { position: absolute; z-index: 1; max-width: min(180px, 80%); border: 1px solid #566d25; border-radius: 6px; padding: 5px 7px; background: #202615; color: #e8edf0; font-size: 11px; font-weight: 850; font-variant-numeric: tabular-nums; line-height: 1.25; pointer-events: none; box-shadow: 0 8px 20px rgba(0, 0, 0, .28); white-space: nowrap; } 210 .metrics-empty { border: 1px dashed #3a4248; border-radius: 8px; padding: 14px; color: #8d989f; background: #111316; } 211 .leaderboard-tabs { width: 100%; display: flex; gap: 12px; margin-bottom: 16px; border-bottom: 1px solid #2f363c; overflow-x: auto; scrollbar-width: none; } 212 .leaderboard-tabs::-webkit-scrollbar { display: none; } 213 .leaderboard-tabs button { flex: 1 0 auto; border: 0; border-bottom: 2px solid transparent; border-radius: 0; padding: 6px 2px 8px; background: transparent; color: #8d989f; font-size: 12px; white-space: nowrap; } 214 .leaderboard-tabs button:hover { border-bottom-color: #566d25; color: #d6dee2; } 215 .leaderboard-tabs button.active { border-bottom-color: #d5f55f; background: transparent; color: #d5f55f; } 216 .leaderboard-grid { width: min(100%, 400px); justify-self: center; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; margin-top: 8px; } 217 .leaderboard-card { min-width: 0; border: 1px solid #2a3035; border-radius: 8px; padding: 12px; background: #181b1f; } 218 .leaderboard-card h3 { margin: 0 0 10px; } 219 .leaderboard-list { display: grid; gap: 8px; } 220 .leaderboard-row { display: grid; grid-template-columns: 54px minmax(0, 1fr) auto; gap: 10px; align-items: center; border: 1px solid #2f363c; border-radius: 8px; padding: 9px; background: #111316; } 221 .leaderboard-row-empty { opacity: .45; } 222 .leaderboard-rank { display: grid; place-items: center; min-height: 30px; border: 1px solid #3a4248; border-radius: 999px; color: #a8b2b8; font-size: 11px; font-weight: 900; } 223 .leaderboard-rank.medal-1 { border-color: #ffd070; background: #2d2513; color: #ffd070; } 224 .leaderboard-rank.medal-2 { border-color: #c7d0d5; background: #20252a; color: #e8edf0; } 225 .leaderboard-rank.medal-3 { border-color: #c69262; background: #2a1f17; color: #ffc18a; } 226 .leaderboard-main { min-width: 0; display: grid; gap: 3px; } 227 .leaderboard-amount { color: #d5f55f; font-size: 13px; font-weight: 900; font-variant-numeric: tabular-nums; white-space: nowrap; } 228 .wallet-grid { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, .8fr); gap: 12px; align-items: start; } 229 .wallet-actions { display: grid; gap: 12px; } 230 .optimize-form { display: grid; gap: 12px; } 231 .optimize-form .optimize-choice { display: flex; align-items: center; gap: 8px; } 232 .optimize-choice input { width: auto; min-width: auto; margin: 0; } 233 .optimize-form button { justify-self: start; } 234 .amount-field { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; } 235 .amount-field label { min-width: 0; } 236 .amount-field input { width: 100%; } 237 .amount-max-button { padding: 9px 10px; border-color: #3a4248; background: #202328; color: #9fa8ad; font-size: 12px; text-transform: uppercase; } 238 .amount-max-button:hover { border-color: #d5f55f; color: #d5f55f; } 239 .advanced-toggle { flex-basis: 100%; width: max-content; align-self: flex-start; border-color: #3a4248; padding: 4px 7px; background: #202328; color: #9fa8ad; font-size: 12px; } 240 .advanced-toggle:hover { border-color: #5a646b; color: #d6dee2; } 241 .send-utxo-list { display: grid; gap: 8px; max-height: 260px; overflow: auto; border: 1px solid #2f363c; border-radius: 8px; padding: 8px; background: #111316; } 242 .send-utxo-list-head { display: flex; justify-content: space-between; gap: 8px; align-items: center; color: #8d989f; font-size: 12px; font-weight: 800; } 243 .send-utxo-actions { display: flex; gap: 6px; align-items: center; } 244 .utxo-select-button { padding: 3px 7px; border-color: #3a4248; background: #202328; color: #9fa8ad; font-size: 12px; } 245 .utxo-select-button:hover { border-color: #5a646b; color: #d6dee2; } 246 .send-utxo-option { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px; align-items: start; border: 1px solid #2f363c; border-radius: 8px; padding: 8px; background: #181b1f; user-select: none; } 247 .send-utxo-option.disabled { border-color: #262c31; background: #14171a; color: #687178; } 248 .send-utxo-option.disabled code, .send-utxo-option.disabled .utxo-node-amount { color: #687178; } 249 .send-utxo-option input { min-width: auto; margin-top: 3px; pointer-events: none; } 250 .utxo-status { color: #8d989f; font-size: 10px; font-weight: 850; text-transform: uppercase; } 251 .send-utxo-summary { flex-basis: 100%; width: 100%; display: grid; gap: 5px; color: #9eb3bc; font-size: 13px; } 252 .wallet-balance-line { display: inline-grid; grid-template-columns: auto auto; gap: 10px; align-items: baseline; padding: 8px 10px; border: 1px solid #2f363c; border-radius: 8px; background: #111316; color: inherit; cursor: pointer; } 253 .wallet-balance-line:hover, .wallet-balance-line:focus-visible { border-color: #d5f55f; outline: none; } 254 .wallet-balance-line .tx-value { font-size: 16px; font-weight: 850; } 255 .mining-grid { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; } 256 .panel-description { max-width: 760px; margin: -4px 0 12px; color: #9eb3bc; font-size: 13px; line-height: 1.45; } 257 .mining-form { width: 100%; display: flex; flex-wrap: wrap; gap: 10px; align-items: end; } 258 .burn-fields { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; } 259 .mine-action-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; align-items: center; } 260 .mine-settings-form { display: grid; gap: 10px; } 261 .mine-fee-fields { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; } 262 .fee-preview { flex-basis: 100%; color: #9eb3bc; font-size: 12px; font-weight: 700; } 263 .fee-preview.error { color: #ffb1a8; } 264 .fee-warning { flex-basis: 100%; color: #ffd070; font-size: 12px; font-weight: 800; } 265 .mine-stats { display: grid; grid-template-columns: repeat(4, minmax(112px, 1fr)); gap: 8px; min-width: 0; } 266 .local-mining-stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); } 267 .fee-history { grid-template-columns: repeat(3, minmax(112px, 1fr)); margin-top: 12px; } 268 .mine-stat { min-width: 0; border: 1px solid #2f363c; border-radius: 8px; padding: 9px 10px; background: #111316; } 269 .mine-stat-label { display: flex; gap: 5px; align-items: center; color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; } 270 .mine-stat-value { margin-top: 5px; color: #dce4e7; font-size: 14px; font-weight: 850; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; } 271 .mine-stat-value.money { color: #d5f55f; } 272 .mine-reward-control { display: grid; gap: 7px; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; } 273 .mine-reward-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; color: #a8b2b8; font-size: 13px; font-weight: 800; } 274 .mine-reward-head strong { color: #d5f55f; font-size: 14px; font-variant-numeric: tabular-nums; } 275 .mine-reward-control input[type="range"] { width: 100%; min-width: 0; padding: 0; accent-color: #d5f55f; } 276 .mine-slider-hints { display: flex; justify-content: space-between; gap: 10px; color: #879198; font-size: 11px; font-weight: 750; } 277 .mine-include-status { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 7px; color: #9eb3bc; font-size: 12px; font-weight: 800; } 278 .mine-include-status.waiting { color: #ffd280; } 279 .mine-include-status.ready { color: #d5f55f; } 280 .mine-include-status.muted { color: #879198; } 281 .mine-save-row { display: flex; justify-content: flex-start; } 282 .mining-event-log { display: grid; gap: 8px; max-height: 360px; overflow: auto; margin-top: 12px; border: 1px solid #2f363c; border-radius: 8px; padding: 8px; background: #0f1114; } 283 .mining-event-log-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; } 284 .mining-event { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: start; border: 1px solid #30383d; border-radius: 8px; padding: 10px; background: #111316; } 285 .mining-event-dot { width: 8px; height: 8px; margin-top: 5px; border-radius: 999px; background: #7f888e; } 286 .mining-event.active .mining-event-dot { background: #d5f55f; box-shadow: 0 0 12px rgba(213, 245, 95, .45); } 287 .mining-event.warning .mining-event-dot { background: #ffd070; } 288 .mining-event.info .mining-event-dot { background: #8de9cd; } 289 .mining-event-title { color: #eef6f8; font-weight: 850; overflow-wrap: anywhere; } 290 .mining-event-detail { margin-top: 3px; color: #9fa8ad; font-size: 12px; line-height: 1.35; overflow-wrap: anywhere; } 291 .mining-event-time { color: #7f888e; font-size: 11px; font-weight: 800; white-space: nowrap; } 292 .mining-event-empty { height: 42px; border: 1px solid #30383d; border-radius: 8px; background: #111316; } 293 .mining-event-empty .skeleton-line { width: 100%; height: 100%; border-radius: 8px; opacity: .55; } 294 .panel-separator { border-top: 1px solid #2f363c; margin: 14px 0 12px; } 295 .stratum-config { display: grid; gap: 10px; } 296 .stratum-note { max-width: 760px; color: #9eb3bc; font-size: 12px; line-height: 1.45; } 297 .stratum-note code { color: #dce4e7; } 298 .stratum-fields { display: grid; gap: 8px; } 299 .stratum-field { display: grid; grid-template-columns: 86px minmax(0, 1fr); gap: 10px; align-items: baseline; min-width: 0; } 300 .stratum-label { color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; } 301 .stratum-value { min-width: 0; color: #dce4e7; font-size: 13px; font-weight: 650; overflow-wrap: anywhere; } 302 .stratum-value.hash { color: #9eb3bc; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; font-weight: 500; } 303 .info-button { display: inline-grid; place-items: center; width: 18px; height: 18px; padding: 0; border-radius: 999px; border-color: #3a4248; background: #181b1f; color: #9eb3bc; font-size: 11px; line-height: 1; } 304 .info-button:hover, .info-button:focus-visible { border-color: #d5f55f; color: #d5f55f; outline: none; } 305 .info-copy { display: grid; gap: 10px; color: #c3cbd0; line-height: 1.45; } 306 .info-copy p { margin: 0; } 307 .info-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; } 308 .info-fact { border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; } 309 .info-fact .label { color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; } 310 .info-fact .value { margin-top: 5px; color: #d5f55f; font-weight: 850; } 311 .mining-head { align-items: center; } 312 .toggle-switch { display: inline-flex; grid-template-columns: none; align-items: center; gap: 9px; color: #9fa8ad; font-size: 12px; font-weight: 850; cursor: pointer; user-select: none; } 313 .toggle-switch input { position: absolute; width: 1px; height: 1px; min-width: 0; margin: 0; opacity: 0; pointer-events: none; } 314 .toggle-track { position: relative; width: 46px; height: 26px; border: 1px solid #3a4248; border-radius: 999px; background: #101215; transition: background .16s ease, border-color .16s ease; } 315 .toggle-thumb { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 999px; background: #879198; transition: transform .16s ease, background .16s ease; } 316 .toggle-switch.active { color: #d5f55f; } 317 .toggle-switch.active .toggle-track { border-color: #d5f55f; background: #263219; } 318 .toggle-switch.active .toggle-thumb { transform: translateX(20px); background: #d5f55f; } 319 .toggle-switch:focus-within .toggle-track { outline: 2px solid #d5f55f; outline-offset: 2px; } 320 .toggle-text { min-width: 22px; text-align: right; } 321 .compact-number-field { display: inline-flex; align-items: center; gap: 8px; color: #9fa8ad; font-size: 12px; font-weight: 850; } 322 .compact-number-field input { width: 58px; min-width: 0; border: 1px solid #3a4248; border-radius: 8px; padding: 7px 8px; background: #101215; color: #dce4e7; font: inherit; font-variant-numeric: tabular-nums; } 323 .compact-number-field input:focus { border-color: #d5f55f; outline: 2px solid rgba(213,245,95,.2); outline-offset: 2px; } 324 .receive-address { display: grid; gap: 8px; } 325 .address-box { border: 1px solid #2f363c; border-radius: 8px; padding: 11px; background: #111316; } 326 .wallet-address-summary { display: flex; justify-content: space-between; gap: 12px; align-items: center; width: 100%; padding: 9px 10px; text-align: left; color: #b9c2c7; background: #111316; border-color: #2f363c; } 327 .wallet-address-summary:hover, .wallet-address-summary:focus-visible { border-color: #59656c; color: #eef6f8; outline: none; } 328 .wallet-address-summary-action { flex: 0 0 auto; color: #d5f55f; font-weight: 800; } 329 .wallet-address-list { display: grid; gap: 8px; } 330 .wallet-address-row { display: grid; gap: 8px; border: 1px solid #2f363c; border-radius: 8px; padding: 11px; background: #111316; } 331 .wallet-address-row-head { display: flex; justify-content: space-between; gap: 10px; align-items: center; } 332 .wallet-address-state { color: #9eb3bc; font-size: 11px; font-weight: 850; text-transform: uppercase; } 333 .wallet-address-state.pending { color: #efca75; } 334 .wallet-address-link { cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-decoration-color: #59656c; text-underline-offset: 3px; } 335 .wallet-address-link:hover, .wallet-address-link:focus-visible { color: #d5f55f; text-decoration-color: #d5f55f; outline: none; } 336 .address-book-list { display: grid; gap: 8px; margin-top: 12px; } 337 .address-book-row { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; text-align: left; border: 1px solid #2f363c; border-radius: 8px; padding: 9px; background: #111316; color: inherit; } 338 .address-book-row:hover { border-color: #4c565c; background: #15181b; } 339 .address-book-row > svg { width: 18px; height: 18px; stroke: currentColor; stroke-width: 2; fill: none; stroke-linecap: round; stroke-linejoin: round; color: #9fa8ad; } 340 .address-book-name { font-weight: 800; color: #eef6f8; overflow-wrap: anywhere; } 341 .address-book-actions { display: flex; gap: 6px; align-items: center; } 342 .address-book-modal { width: min(560px, 100%); } 343 .address-book-modal form { width: 100%; display: grid; gap: 10px; } 344 .address-book-form { display: grid; gap: 10px; margin-top: 14px; padding-top: 14px; border-top: 1px solid #2f363c; } 345 .address-book-modal-actions { flex: 1 0 100%; width: 100%; display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } 346 .address-book-picker-list { display: grid; gap: 8px; } 347 .address-book-picker-row { width: 100%; display: grid; gap: 3px; text-align: left; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; color: inherit; } 348 .address-book-picker-row:hover { border-color: #4c565c; background: #15181b; } 349 .recipient-field { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; } 350 .icon-button { display: inline-grid; place-items: center; width: 38px; height: 38px; padding: 0; line-height: 0; border-radius: 8px; } 351 .icon-button svg { width: 19px; height: 19px; stroke: currentColor; stroke-width: 2; fill: none; stroke-linecap: round; stroke-linejoin: round; } 352 .modal-delete-button { color: #ffb4b4; } 353 input.invalid { border-color: #e36a6a; outline: 2px solid rgba(227,106,106,.16); outline-offset: 2px; } 354 .panel-head { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 12px; } 355 .panel-head h2, .panel-head h3 { margin-bottom: 0; } 356 .switch { display: inline-flex; grid-template-columns: none; align-items: center; gap: 8px; color: #d6dee2; font-weight: 700; } 357 .switch input { width: auto; min-width: 0; accent-color: #d5f55f; } 358 .wallet-tx-panel { min-width: 0; overflow: hidden; } 359 .wallet-tx-panel .panel-head { flex-wrap: wrap; } 360 .wallet-tx-filters { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; align-items: center; } 361 .tx-filter { display: inline-flex; align-items: center; gap: 6px; border: 1px solid #3a4248; border-radius: 999px; padding: 4px 8px; color: #9fa8ad; background: #111316; font-size: 12px; font-weight: 850; cursor: pointer; user-select: none; } 362 .tx-filter input { position: absolute; width: 1px; height: 1px; min-width: 0; margin: 0; opacity: 0; pointer-events: none; } 363 .tx-filter.active { border-color: #d5f55f; background: #202616; color: #d5f55f; } 364 .tx-filter:focus-within { outline: 2px solid #d5f55f; outline-offset: 2px; } 365 .wallet-tx-list { max-height: min(620px, calc(100vh - 220px)); min-width: 0; display: grid; gap: 8px; overflow-y: auto; overscroll-behavior-y: contain; padding-right: 4px; } 366 .wallet-tx-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; align-items: start; border: 1px solid #2f363c; border-radius: 8px; padding: 12px; background: #111316; cursor: pointer; text-align: left; } 367 .wallet-tx-row:hover, .wallet-tx-row:focus-visible, .tx-card:hover, .tx-card:focus-visible, .mempool-item:hover, .mempool-item:focus-visible { border-color: #d5f55f; box-shadow: 0 0 0 1px rgba(213, 245, 95, .22); outline: none; } 368 .wallet-tx-row.pending { border-color: #3a4147; background: #191c20; box-shadow: inset 3px 0 0 #6f7880; } 369 .wallet-tx-row .pill { position: absolute; top: 10px; right: 10px; } 370 .wallet-tx-main { display: grid; gap: 5px; min-width: 0; padding-right: 92px; } 371 .tx-field { display: grid; grid-template-columns: 74px minmax(0, 1fr); gap: 8px; align-items: baseline; min-width: 0; } 372 .tx-label { color: #879198; font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0; } 373 .tx-value { min-width: 0; color: #dce4e7; font-size: 13px; font-weight: 600; overflow-wrap: anywhere; } 374 .tx-value.money { color: #d5f55f; font-variant-numeric: tabular-nums; } 375 .tx-value.hash { color: #9eb3bc; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; font-weight: 500; } 376 .tx-value.number { color: #c7d0d5; font-variant-numeric: tabular-nums; } 377 .tx-value.text { color: #e8edf0; } 378 .metric-context { display: grid; gap: 5px; margin-top: 12px; } 379 .peer-toolbar { display: flex; justify-content: space-between; gap: 12px; align-items: start; flex-wrap: wrap; margin-bottom: 12px; } 380 .peer-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 8px; margin-bottom: 12px; } 381 .peer-summary-item { min-width: 0; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; } 382 .peer-summary-label { color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; } 383 .peer-summary-value { margin-top: 5px; color: #dce4e7; font-size: 15px; font-weight: 850; } 384 .peer-form { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; } 385 .peer-form label { min-width: min(320px, 100%); } 386 .peer-form input { width: 100%; } 387 .peer-status { display: inline-flex; align-items: center; border: 1px solid #3a4248; border-radius: 999px; padding: 3px 8px; color: #a8b2b8; font-size: 11px; font-weight: 850; } 388 .peer-status.synced, .peer-status.active { border-color: #566d25; color: #d5f55f; background: #1c2516; } 389 .peer-status.stale { border-color: #5f5125; color: #ffe08a; background: #211d12; } 390 .peer-status.banned { border-color: #713434; color: #ffb1a8; background: #2a1717; } 391 .peer-status.error { border-color: #713434; color: #ffb1a8; background: #2a1717; } 392 .peer-actions { display: flex; gap: 6px; align-items: center; } 393 .peer-address { display: inline-flex; gap: 7px; align-items: center; flex-wrap: wrap; } 394 .country-code { border: 1px solid #3a4248; border-radius: 4px; padding: 1px 5px; color: #a8b2b8; font-size: 10px; font-weight: 850; letter-spacing: .06em; } 395 .peer-details { padding: 4px 7px; font-size: 12px; } 396 .peer-remove { padding: 4px 7px; border-color: #4f3737; background: #221717; color: #ffb1a8; font-size: 12px; } 397 .peer-remove:hover { border-color: #ffb1a8; color: #ffd4cf; } 398 .peer-modal-section { display: grid; gap: 8px; margin-top: 14px; } 399 .peer-modal-section h3 { margin: 0; color: #8d989f; font-size: 11px; text-transform: uppercase; } 400 .peer-modal-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 8px; } 401 .peer-modal-field { min-width: 0; display: grid; gap: 5px; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; } 402 .peer-modal-field.wide { grid-column: 1 / -1; } 403 .peer-modal-value { min-width: 0; overflow-wrap: anywhere; } 404 .peer-capabilities { display: flex; flex-wrap: wrap; gap: 6px; } 405 .network-health { display: grid; grid-template-columns: minmax(180px, .8fr) minmax(0, 1.2fr); gap: 12px; align-items: stretch; margin-bottom: 12px; } 406 .network-health-state { display: grid; align-content: center; gap: 5px; border: 1px solid #3a4248; border-radius: 8px; padding: 12px; background: #111316; } 407 .network-health-state.healthy { border-color: #566d25; background: #182112; } 408 .network-health-state.syncing, .network-health-state.stale { border-color: #5f5125; background: #211d12; } 409 .network-health-state.isolated, .network-health-state.error, .network-health-state.banned { border-color: #713434; background: #241716; } 410 .network-health-label { color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; } 411 .network-health-value { color: #e8edf0; font-size: 20px; font-weight: 900; text-transform: capitalize; } 412 .network-health-detail { color: #a8b2b8; font-size: 12px; } 413 .network-health-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(116px, 1fr)); gap: 8px; } 414 .panel .grid + form { margin-top: 12px; } 415 .explorer-shell { width: 100%; display: grid; gap: 12px; } 416 .block-rail-wrap { background: #181b1f; border: 1px solid #2a3035; border-radius: 8px; padding: 12px; overflow: hidden; } 417 .block-rail-head { display: flex; justify-content: space-between; gap: 10px; align-items: center; margin-bottom: 10px; } 418 .block-rail { display: flex; gap: 8px; overflow-x: auto; padding: 1px 0 10px; scroll-snap-type: x proximity; } 419 .block-card { flex: 0 0 122px; min-height: 100px; display: grid; gap: 6px; border: 1px solid #2f363c; border-radius: 8px; padding: 9px; background: #111316; color: #e8edf0; text-align: left; scroll-snap-align: start; } 420 button.block-card { cursor: pointer; user-select: none; } 421 .block-card:hover { border-color: #d5f55f; color: #d5f55f; } 422 .block-card.selected { background: #202616; border-color: #d5f55f; box-shadow: inset 0 0 0 1px #d5f55f; } 423 .block-card.new-block { animation: block-arrive .45s ease both; } 424 .block-card-select { display: grid; gap: 6px; padding: 0; border: 0; background: transparent; color: inherit; text-align: left; } 425 @keyframes block-arrive { from { opacity: .2; transform: translateX(-12px); } to { opacity: 1; transform: translateX(0); } } 426 .block-height { font-size: 18px; font-weight: 900; } 427 .block-meta { display: flex; gap: 8px; color: #8d989f; font-size: 12px; } 428 .block-miner { width: 100%; padding: 0; border: 0; background: transparent; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; text-align: left; overflow-wrap: anywhere; color: #9eb3bc; } 429 .block-skeleton-group { flex: 0 0 auto; display: flex; gap: 8px; } 430 .block-card-skeleton { grid-template-rows: 22px 18px minmax(28px, 1fr); align-items: start; cursor: default; } 431 .skeleton-block-height { width: 46px; height: 22px; border-radius: 6px; background: #30383d; } 432 .skeleton-block-meta { display: flex; gap: 6px; min-width: 0; } 433 .skeleton-pill { flex: 0 0 auto; width: 18px; height: 14px; border-radius: 999px; background: #2b3136; } 434 .skeleton-pill.wide { width: 28px; } 435 .skeleton-block-miner { align-self: stretch; min-height: 28px; border-radius: 6px; background: #242a2f; } 436 .skeleton-card { pointer-events: none; position: relative; overflow: hidden; } 437 .skeleton-card::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(213, 245, 95, .12), transparent); animation: skeleton-sweep 1.15s ease-in-out infinite; } 438 @keyframes skeleton-sweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } } 439 .skeleton-line { height: 12px; border-radius: 6px; background: #2b3136; } 440 .skeleton-line.short { width: 42%; } 441 .skeleton-line.medium { width: 68%; } 442 .skeleton-line.long { width: 88%; } 443 .page-sentinel { min-height: 1px; } 444 .block-page-sentinel { flex: 0 0 1px; min-height: 100px; } 445 .dataset-loader { display: grid; gap: 8px; min-width: 0; } 446 .skeleton-table-cell { height: 12px; width: 100%; border-radius: 6px; background: #2b3136; } 447 tr.skeleton-card td { padding-top: 11px; padding-bottom: 11px; } 448 .detail-grid { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: 12px; } 449 .detail-kv { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 8px; font-size: 13px; margin: 7px 0; } 450 .detail-kv .key { color: #8d989f; } 451 .detail-link { width: fit-content; max-width: 100%; padding: 0; border: 0; background: transparent; color: #d7f2ff; font: inherit; text-align: left; cursor: pointer; } 452 .detail-link code, .detail-link .link-value { color: inherit; text-decoration: underline; text-underline-offset: 3px; } 453 .detail-link:hover { color: #d5f55f; } 454 .rank-list { display: grid; gap: 8px; } 455 .rank-row { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 10px; align-items: start; border: 1px solid #30383d; border-radius: 8px; padding: 10px; background: #15191d; } 456 .rank-number { color: #d7f2ff; font-weight: 700; } 457 .rank-details { display: grid; gap: 6px; min-width: 0; } 458 .bundle-summary { display: grid; gap: 5px; border: 1px solid #425027; border-radius: 8px; padding: 12px; margin-bottom: 14px; background: #1b2116; } 459 .bundle-summary strong { color: #d5f55f; } 460 .bundle-flow { display: grid; grid-template-columns: repeat(auto-fit, minmax(175px, 1fr)); gap: 8px; margin-bottom: 14px; } 461 .bundle-slot { display: grid; align-content: start; gap: 8px; min-width: 0; border: 1px solid #30383d; border-radius: 8px; padding: 11px; background: #111316; } 462 .bundle-slot-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; } 463 .bundle-slot-number { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; background: #d5f55f; color: #111316; font-size: 12px; font-weight: 900; } 464 .bundle-slot-status { color: #9fbd42; font-size: 11px; font-weight: 850; text-transform: uppercase; } 465 .bundle-slot .tx-field { grid-template-columns: minmax(0, 1fr); gap: 3px; align-items: start; } 466 .bundle-slot code { overflow-wrap: anywhere; color: #9eb3bc; font-size: 11px; } 467 .bundle-how { display: grid; gap: 8px; border-top: 1px solid #30383d; padding-top: 14px; } 468 .bundle-how h3 { margin: 0 0 2px; } 469 .bundle-how-step { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 8px; align-items: start; color: #cbd3d7; } 470 .bundle-how-step > span:first-child { display: inline-grid; place-items: center; width: 22px; height: 22px; border: 1px solid #4a555b; border-radius: 50%; color: #d7f2ff; font-size: 11px; font-weight: 800; } 471 .tx-list { display: grid; gap: 8px; } 472 .tx-section { display: grid; gap: 8px; } 473 .tx-section + .tx-section { margin-top: 10px; padding-top: 10px; border-top: 1px solid #2f363c; } 474 .tx-section-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: #f4f7f8; font-size: 13px; font-weight: 800; } 475 summary.tx-section-title { cursor: pointer; } 476 details.tx-section:not([open]) { gap: 0; } 477 .tx-section-meta { color: #8e979e; font-size: 12px; font-weight: 600; } 478 .tx-scroll-list { display: grid; gap: 8px; max-height: min(430px, calc(100vh - 300px)); min-height: 0; overflow-y: auto; overscroll-behavior-y: contain; padding-right: 4px; } 479 .tx-card, .mempool-item { position: relative; display: grid; align-content: start; grid-auto-rows: min-content; gap: 6px; border: 1px solid #2f363c; border-radius: 8px; padding: 12px; background: #111316; cursor: pointer; text-align: left; } 480 .mempool-item.before-last-block { opacity: .56; } 481 .mempool-item.new-since-block { background: #151a12; opacity: 1; } 482 .mempool-item.new-since-block::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; border-radius: 8px 0 0 8px; background: #d5f55f; } 483 .mempool-state { color: #d5f55f; font-size: 10px; font-weight: 850; text-transform: uppercase; } 484 .mempool-time { color: #8d989f; font-size: 11px; font-weight: 700; } 485 .mempool-top { display: flex; justify-content: space-between; gap: 8px; align-items: flex-start; min-width: 0; } 486 .mempool-top-meta { display: grid; gap: 3px; min-width: 0; } 487 .tx-card .pill, .mempool-item .pill { position: absolute; top: 10px; right: 10px; } 488 .mempool-item .pill { position: static; flex: 0 0 auto; } 489 .pill { display: inline-flex; align-items: center; border-radius: 999px; padding: 2px 8px; font-size: 12px; font-weight: 800; background: #2b3136; color: #d6dee2; } 490 .pill.burn { background: #332918; color: #ffd070; } 491 .pill.transfer { background: #17312a; color: #8de9cd; } 492 .pill.mine { background: #172a34; color: #8bdcff; } 493 .pill.reward { background: #263016; color: #d5f55f; } 494 .pill.error { background: #341918; color: #ffb1a8; } 495 .mempool-panel { min-width: 0; overflow: hidden; } 496 .mempool-strip { width: 100%; min-width: 0; display: flex; gap: 8px; overflow-x: auto; overscroll-behavior-x: contain; padding: 1px 0 10px; scroll-snap-type: x proximity; } 497 .mempool-item { flex: 0 0 220px; scroll-snap-align: start; align-self: stretch; } 498 .tx-modal { width: min(940px, 100%); max-height: calc(100vh - 44px); overflow: auto; border: 1px solid #3b4448; border-radius: 8px; padding: 16px; background: #181b1f; box-shadow: 0 24px 80px rgba(0, 0, 0, .46); } 499 .tx-modal-head { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; margin-bottom: 14px; } 500 .tx-modal-title { display: grid; justify-items: start; gap: 6px; min-width: 0; } 501 .tx-modal-title h2 { margin: 0; } 502 .tx-modal-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; margin-bottom: 12px; } 503 .utxo-flow { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 12px; align-items: stretch; } 504 .utxo-column { display: grid; align-content: start; gap: 8px; min-width: 0; } 505 .utxo-column h3 { margin: 0; color: #8d989f; font-size: 11px; text-transform: uppercase; } 506 .utxo-node { display: grid; gap: 5px; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; min-width: 0; } 507 .utxo-node.burned { border-color: #5e4821; background: #1f1a12; } 508 .utxo-node.fee { border-color: #4b5260; background: #171a20; } 509 .utxo-node.reward { border-color: #526329; background: #1a2013; } 510 .utxo-node-label { display: flex; justify-content: space-between; gap: 8px; color: #8d989f; font-size: 11px; font-weight: 800; text-transform: uppercase; } 511 .utxo-node-amount { color: #d5f55f; font-weight: 850; font-variant-numeric: tabular-nums; } 512 .utxo-node-address, .utxo-node-ref { color: #9eb3bc; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; overflow-wrap: anywhere; } 513 .utxo-arrow { display: grid; place-items: center; color: #d5f55f; font-size: 24px; font-weight: 900; } 514 .tx-modal-empty { border: 1px dashed #3a4248; border-radius: 8px; padding: 10px; color: #8d989f; } 515 .utxo-list { display: grid; gap: 8px; } 516 .wallet-utxo-row { display: grid; gap: 6px; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; } 517 @media (max-width: 760px) { .utxo-flow, .mine-action-row, .mine-stats { grid-template-columns: 1fr; } .utxo-arrow { min-height: 28px; transform: rotate(90deg); } .tx-modal-head { align-items: stretch; } } 518 @media (max-width: 920px) { .setup-grid, .wallet-grid, .mining-grid, .detail-grid, .network-health { grid-template-columns: 1fr; } } 519 @media (max-width: 760px) { 520 body { padding-bottom: calc(68px + env(safe-area-inset-bottom)); } 521 .app-shell { display: block; min-height: calc(100vh - 68px - env(safe-area-inset-bottom)); min-height: calc(100dvh - 68px - env(safe-area-inset-bottom)); overflow: visible; } 522 .sidebar { position: fixed; z-index: 20; inset: auto 0 0; width: 100%; height: auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(34px, 1fr)); gap: 4px; padding: 6px max(8px, env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); border: 0; border-top: 1px solid #30363b; background: rgba(21, 23, 26, .96); box-shadow: 0 -12px 32px rgba(0, 0, 0, .28); backdrop-filter: blur(14px); } 523 .version-panel { display: none; } 524 .brand-mark { width: 100%; min-height: 50px; padding: 5px 2px; } 525 .side-nav { display: contents; } 526 .nav-button { width: 100%; min-width: 0; min-height: 50px; gap: 2px; padding: 5px 2px; } 527 .nav-button span { font-size: 10px; } 528 .discord-button, .settings-button { width: 100%; min-width: 0; min-height: 50px; height: 50px; gap: 2px; padding: 5px 2px; } 529 .sidebar-bottom-actions { display: contents; } 530 .settings-button svg { width: 20px; height: 20px; } 531 .settings-button span { display: block; } 532 .content { min-height: calc(100vh - 68px - env(safe-area-inset-bottom)); min-height: calc(100dvh - 68px - env(safe-area-inset-bottom)); padding: 14px 12px 24px; } 533 header, .split, .setup-grid, .wallet-grid, .mining-grid, .detail-grid, .wallet-tx-row { grid-template-columns: 1fr; } 534 header { display: flex; align-items: flex-start; padding-bottom: 14px; } 535 h1 { font-size: 24px; } 536 h2 { font-size: 17px; } 537 .header-actions { flex-direction: column-reverse; gap: 6px; align-items: flex-end; text-align: right; font-size: 12px; } 538 .basic-status-row { flex-wrap: wrap; } 539 .page-title { margin-bottom: 12px; } 540 .panel, .metric { padding: 12px; } 541 .panel-head { align-items: flex-start; } 542 .settings-mode-row { flex-direction: column; align-items: stretch; } 543 .settings-mode-row .segmented { width: 100%; } 544 .settings-mode-row .segmented button { flex: 1; } 545 .metrics-head { align-items: flex-start; flex-direction: column; } 546 .metrics-range { width: 100%; } 547 .metrics-range button { flex: 1 1 0; } 548 .segmented.setup-mode-picker { grid-template-columns: 1fr; } 549 .metrics-grid { grid-template-columns: 1fr; } 550 form { width: 100%; align-items: stretch; } 551 form > label, .burn-fields, .mine-fee-fields, .recipient-field, .amount-field, .send-utxo-summary { width: 100%; } 552 .burn-fields, .mine-fee-fields { display: grid; grid-template-columns: 1fr; align-items: stretch; } 553 input, textarea { min-width: 0; width: 100%; font-size: 16px; } 554 .switch input { width: auto; } 555 form > button.primary, .burn-fields > button.primary, .setup-actions > button.primary { min-height: 44px; } 556 .peer-toolbar { display: grid; } 557 .peer-form { display: grid; grid-template-columns: 1fr; } 558 .peer-form label { min-width: 0; } 559 .peer-form button { min-height: 44px; } 560 .peer-table, .peer-table tbody, .peer-table tr, .peer-table td { display: block; width: 100%; } 561 .peer-table { margin: 0; } 562 .peer-table thead { display: none; } 563 .peer-table tbody { display: grid; gap: 10px; } 564 .peer-table tr { overflow: hidden; border: 1px solid #2f363c; border-radius: 8px; background: #111316; } 565 .peer-table td { display: grid; grid-template-columns: minmax(82px, .38fr) minmax(0, 1fr); gap: 10px; align-items: baseline; border-bottom: 1px solid #2a3035; padding: 9px 10px; overflow-wrap: anywhere; } 566 .peer-table td::before { content: attr(data-label); color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; } 567 .peer-table td:last-child { border-bottom: 0; } 568 .peer-table td[colspan] { display: block; } 569 .peer-table td[colspan]::before { display: none; } 570 .peer-actions { justify-content: flex-start; } 571 .seed-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } 572 .block-card { flex-basis: 108px; } 573 .setup-overlay { place-items: end center; padding: 8px; } 574 .setup-modal, .tx-modal { width: 100%; max-height: calc(100vh - 16px); max-height: calc(100dvh - 16px); border-radius: 12px; padding: 14px; } 575 .setup-actions, .danger-actions, .address-book-modal-actions { flex-wrap: wrap; } 576 .setup-actions button, .danger-actions button, .address-book-modal-actions button { min-height: 44px; } 577 .tx-modal-head { flex-direction: column; } 578 .tx-modal-head > button { width: 100%; min-height: 44px; } 579 .flash { top: 10px; right: 10px; width: calc(100vw - 20px); } 580 .wallet-tx-list, .tx-scroll-list { max-height: none; overflow-y: visible; padding-right: 0; } 581 .mining-event { grid-template-columns: auto minmax(0, 1fr); } 582 .mining-event-time { grid-column: 2; } 583 .dashboard-grid { grid-template-columns: repeat(2, minmax(0, 156px)); } 584 .dashboard-card { aspect-ratio: 1; min-height: 0; } 585 } 586 @media (max-width: 420px) { 587 .content { padding-inline: 9px; } 588 .sidebar { gap: 2px; padding-inline: 4px; } 589 .nav-button, .discord-button, .settings-button { min-height: 48px; } 590 .nav-button svg, .discord-button svg, .settings-button svg { width: 19px; height: 19px; } 591 .nav-button span, .discord-button span, .settings-button span { font-size: 9px; } 592 .panel, .metric, .block-rail-wrap { border-radius: 7px; padding: 10px; } 593 .grid, .peer-summary, .network-health-grid, .metrics-summary, .tx-modal-summary { grid-template-columns: 1fr 1fr; } 594 .wallet-balance-line { width: 100%; justify-content: space-between; } 595 .wallet-tx-main { padding-right: 0; padding-top: 24px; } 596 .wallet-tx-row .pill { top: 9px; left: 9px; right: auto; } 597 .tx-field, .detail-kv, .stratum-field { grid-template-columns: 1fr; gap: 3px; } 598 .seed-grid { grid-template-columns: 1fr; } 599 } 600 </style> 601 <script defer src="/assets/iuna-ui.js?v="#, 602 env!("CARGO_PKG_VERSION"), 603 r#""></script> 604 <script defer src="/assets/alpine.min.js"></script> 605 </head> 606 <body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak> 607 <div class="app-shell" :inert="syncingNode()"> 608 <aside class="sidebar" aria-label="iuna navigation"> 609 <button class="brand-mark" :class="{ active: tab === 'dashboard' }" type="button" @click="setTab('dashboard')" title="Dashboard" aria-label="Dashboard"><svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><circle class="mark-dot" cx="9.4" cy="7.6" r="2.8"></circle><path class="mark-loop" d="M9.4 13v7.1c0 3.7 2.9 6.4 6.6 6.4s6.6-2.7 6.6-6.4V13"></path></svg></button> 610 <nav class="side-nav"> 611 <button class="nav-button" :class="{ active: tab === 'wallet' }" @click="setTab('wallet')" type="button" title="Wallet" aria-label="Wallet"> 612 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7h16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H3z"></path><path d="M3 7V5a2 2 0 0 1 2-2h12"></path><path d="M16 13h3"></path></svg> 613 <span>Wallet</span> 614 </button> 615 <button class="nav-button" x-show="advancedMode()" :class="{ active: tab === 'mining' }" @click="setTab('mining')" type="button" title="Mining" aria-label="Mining"> 616 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 19V5"></path><path d="M4 19h16"></path><path d="M7 15l4-4 3 3 5-7"></path></svg> 617 <span>Mining</span> 618 </button> 619 <button class="nav-button" :class="{ active: tab === 'p2p' }" @click="setTab('p2p')" type="button" title="P2P" aria-label="P2P"> 620 <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="6" r="3"></circle><circle cx="18" cy="18" r="3"></circle><path d="M8.5 10.5 15.5 7.5"></path><path d="M8.5 13.5 15.5 16.5"></path></svg> 621 <span>P2P</span> 622 </button> 623 <button class="nav-button" :class="{ active: tab === 'chain' }" @click="setTab('chain')" type="button" title="Explorer" aria-label="Explorer"> 624 <svg class="chain-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="1.5" y="9" width="5.5" height="5.5"></rect><rect x="9.25" y="9" width="5.5" height="5.5"></rect><rect x="17" y="9" width="5.5" height="5.5"></rect></svg> 625 <span>Chain</span> 626 </button> 627 <button class="nav-button" x-show="developmentMode()" :class="{ active: tab === 'metrics' }" @click="setTab('metrics')" type="button" title="Metrics" aria-label="Metrics"> 628 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 19V5"></path><path d="M4 19h16"></path><path d="M7 15l3-4 3 2 4-7"></path><path d="M7 17h10"></path></svg> 629 <span>Metrics</span> 630 </button> 631 <button class="nav-button" x-show="developmentMode()" :class="{ active: tab === 'leaderboards' }" @click="setTab('leaderboards')" type="button" title="Leaderboards" aria-label="Leaderboards"> 632 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8v4a4 4 0 0 1-8 0V4Z"></path><path d="M8 6H5v1a4 4 0 0 0 4 4"></path><path d="M16 6h3v1a4 4 0 0 1-4 4"></path><path d="M12 12v4"></path><path d="M8 20h8"></path><path d="M9 16h6v4H9z"></path></svg> 633 <span>Leaders</span> 634 </button> 635 </nav> 636 <div class="sidebar-bottom-actions"> 637 <button class="settings-button" :class="{ active: tab === 'settings' }" type="button" @click="setTab('settings')" title="Settings" aria-label="Settings"> 638 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.7 3.2 9.2 5.5a7.2 7.2 0 0 0-1.4.8L5.6 5.6 3.2 9.8l1.7 1.6a7.8 7.8 0 0 0 0 1.6l-1.7 1.6 2.4 4.2 2.2-.7a7.2 7.2 0 0 0 1.4.8l.5 2.3h4.8l.5-2.3a7.2 7.2 0 0 0 1.4-.8l2.2.7 2.4-4.2-1.7-1.6a7.8 7.8 0 0 0 0-1.6L21 9.8l-2.4-4.2-2.2.7a7.2 7.2 0 0 0-1.4-.8l-.5-2.3H9.7Z"></path><circle cx="12" cy="12.2" r="3.1"></circle></svg> 639 <span>Settings</span> 640 </button> 641 <button class="version-panel" type="button" :disabled="desktopUpdateBusy" :class="{ update: updateAvailable(), checking: releaseCheckState === 'checking', failed: releaseCheckState === 'failed' }" :title="versionPanelTitle()" @click="openLatestRelease"> 642 <span class="version-dot" aria-hidden="true"></span> 643 <span class="version-label" x-text="appVersionLabel()"></span> 644 <span class="version-update" x-show="updateAvailable()">Update</span> 645 </button> 646 <a class="discord-button" href="https://discord.gg/wdk8cjkj2G" target="_blank" rel="noopener noreferrer" title="Discord" aria-label="Discord"> 647 <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19.54 5.34A16.9 16.9 0 0 0 15.35 4a11.7 11.7 0 0 0-.54 1.1 15.8 15.8 0 0 0-4.62 0A11.7 11.7 0 0 0 9.65 4a16.9 16.9 0 0 0-4.19 1.34C2.81 9.28 2.09 13.12 2.45 16.9A16.8 16.8 0 0 0 7.59 19.5a12.8 12.8 0 0 0 1.1-1.79 10.9 10.9 0 0 1-1.73-.83c.14-.1.28-.21.42-.32a12.1 12.1 0 0 0 9.24 0c.14.11.28.22.42.32-.55.32-1.13.6-1.74.83.32.63.69 1.23 1.1 1.79a16.8 16.8 0 0 0 5.15-2.6c.42-4.38-.72-8.18-2.01-11.56ZM9.32 14.57c-1 0-1.82-.92-1.82-2.04 0-1.13.8-2.05 1.82-2.05 1.02 0 1.84.92 1.82 2.05 0 1.12-.8 2.04-1.82 2.04Zm5.36 0c-1 0-1.82-.92-1.82-2.04 0-1.13.8-2.05 1.82-2.05 1.02 0 1.84.92 1.82 2.05 0 1.12-.8 2.04-1.82 2.04Z"></path></svg> 648 <span>Discord</span> 649 </a> 650 </div> 651 </aside> 652 653 <main class="content"> 654 <header> 655 <div> 656 <h1 x-text="pageTitle()">iuna</h1> 657 <div class="basic-status-row" x-show="basicMode()"> 658 <span class="basic-status" :class="networkHealthClass()" x-text="basicNetworkStatusLabel()"></span> 659 <button class="basic-status-detail" type="button" x-show="basicNetworkNeedsAttention()" @click="setTab('p2p')">Details</button> 660 </div> 661 </div> 662 <div class="header-actions"> 663 <div class="muted" x-text="lastUpdatedLabel()"></div> 664 <button class="lock-button" type="button" x-show="auth.authenticated" @click="logout">Lock</button> 665 </div> 666 </header> 667 668 <div class="flash" :class="flash?.kind" x-show="flash" x-transition x-text="flash?.message"></div> 669 <div class="persistent-banner" x-show="p2pRestartRequired()" x-transition x-text="p2pRestartMessage()"></div> 670 671 <section class="dashboard-section" x-show="tab === 'dashboard'"> 672 <div class="dashboard-grid"> 673 <article class="dashboard-card balance good"> 674 <div class="dashboard-card-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 7.5h14a2 2 0 0 1 2 2v9H4a2 2 0 0 1-2-2v-11a2 2 0 0 1 2-2h13"></path><path d="M15 12h5"></path><circle cx="15" cy="12" r=".5"></circle></svg></div> 675 <div class="dashboard-card-copy"> 676 <div class="dashboard-card-label">Current balance</div> 677 <div class="dashboard-card-value">IUNA <span x-text="amountLabel(status.wallet_balance)"></span></div> 678 </div> 679 </article> 680 681 <article class="dashboard-card" :class="dashboardMiningState()"> 682 <div class="dashboard-card-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m14 13-8.381 8.38a1 1 0 0 1-3.001-3L11 9.999"></path><path d="M15.973 4.027A13 13 0 0 0 5.902 2.373c-1.398.342-1.092 2.158.277 2.601a19.9 19.9 0 0 1 5.822 3.024"></path><path d="M16.001 11.999a19.9 19.9 0 0 1 3.024 5.824c.444 1.369 2.26 1.676 2.603.278A13 13 0 0 0 20 8.069"></path><path d="M18.352 3.352a1.205 1.205 0 0 0-1.704 0l-5.296 5.296a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l5.296-5.296a1.205 1.205 0 0 0 0-1.704z"></path></svg></div> 683 <div class="dashboard-card-copy"> 684 <div class="dashboard-card-label">Mining</div> 685 <div class="dashboard-status" x-text="dashboardMiningLabel()"></div> 686 </div> 687 </article> 688 689 <article class="dashboard-card burning" :class="dashboardBurnState()"> 690 <div class="dashboard-card-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 12c2-3 0-7-1-8 0 3-1.8 4.7-3 6s-2 3.2-2 5a6 6 0 1 0 12 0c0-1.5-1.1-3.9-2-5 0 3-1.7 4-3 4s-2-1-1-2Z"></path></svg></div> 691 <div class="dashboard-card-copy"> 692 <div class="dashboard-card-label">Burning</div> 693 <div class="dashboard-status" x-text="dashboardBurnLabel()"></div> 694 </div> 695 </article> 696 697 <article class="dashboard-card" :class="dashboardBlockState()"> 698 <div class="dashboard-card-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m12 2.8 8 4.4v9.6l-8 4.4-8-4.4V7.2z"></path><path d="m4.3 7.4 7.7 4.3 7.7-4.3M12 11.7v9.1"></path></svg></div> 699 <div class="dashboard-card-copy"> 700 <div class="dashboard-card-label">Last block</div> 701 <div class="dashboard-status" x-text="dashboardLastBlockLabel()"></div> 702 </div> 703 </article> 704 705 <article class="dashboard-card" :class="dashboardNetworkState()"> 706 <div class="dashboard-card-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="5" cy="12" r="2.5"></circle><circle cx="19" cy="6" r="2.5"></circle><circle cx="19" cy="18" r="2.5"></circle><path d="m7.3 11 9.4-4M7.3 13l9.4 4"></path></svg></div> 707 <div class="dashboard-card-copy"> 708 <div class="dashboard-card-label">Network</div> 709 <div class="dashboard-status" x-text="basicNetworkStatusLabel()"></div> 710 </div> 711 </article> 712 </div> 713 </section> 714 715 <section x-show="tab === 'wallet'"> 716 <div class="page-title"> 717 <button class="wallet-balance-line" type="button" @click="openWalletUtxosModal" title="Show wallet UTXOs"> 718 <span class="tx-label">Balance</span> 719 <span class="tx-value money">IUNA <span x-text="amountLabel(status.wallet_balance)"></span></span> 720 </button> 721 </div> 722 <div class="panel" x-show="showOptimizeSuggestion()"> 723 <h3>Your wallet could be more efficient</h3> 724 <p class="panel-description">Your balance consists of <span x-text="walletUtxoPage.total"></span> parts. Combining them can reduce the number of inputs needed for future payments. Review the network fee before deciding.</p> 725 <button type="button" @click="openOptimizeWallet">Review optimization</button> 726 <button type="button" @click="dismissOptimizeSuggestion">Later</button> 727 </div> 728 <div class="panel" x-show="status.quantum_migration?.active && status.quantum_migration?.legacy_utxos > 0"> 729 <h3>Quantum-resistant wallet migration</h3> 730 <p class="panel-description">Transaction v2 is active. Review how your legacy Ed25519 balance can move to the hybrid Ed25519 + ML-DSA wallet.</p> 731 <div class="detail-grid"> 732 <div> 733 <div class="detail-kv"><div class="key">Legacy balance</div><div>IUNA <span x-text="amountLabel(status.quantum_migration?.legacy_balance || 0)"></span></div></div> 734 <div class="detail-kv"><div class="key">Hybrid balance</div><div>IUNA <span x-text="amountLabel(status.quantum_migration?.hybrid_balance || 0)"></span></div></div> 735 <div class="detail-kv"><div class="key">Legacy UTXOs</div><div x-text="status.quantum_migration?.legacy_utxos || 0"></div></div> 736 <div class="detail-kv"><div class="key">Hybrid address</div><code x-text="status.quantum_migration?.hybrid_address || 'Unlock wallet to derive'"></code></div> 737 </div> 738 <form @submit.prevent="previewQuantumMigration" x-show="status.quantum_migration?.legacy_balance > 0"> 739 <label>Fee / byte<input x-model="quantumMigrationFee" type="number" min="0.000001" step="0.000001" required></label> 740 <button type="submit" :disabled="quantumMigrationBusy || quantumMigrationSubmitting || status.wallet_locked || status.quantum_migration?.migration_pending" x-text="quantumMigrationBusy ? 'Calculatingā¦' : 'Preview migration'"></button> 741 <div class="fee-warning" role="alert" x-show="quantumMigrationError" x-text="quantumMigrationError"></div> 742 <div class="muted" x-show="status.quantum_migration?.migration_pending"> 743 A migration batch is saved in this node's transaction-v2 mempool and will be rebroadcast until confirmation. 744 <code x-show="status.quantum_migration?.pending_transaction_id" x-text="status.quantum_migration?.pending_transaction_id || ''"></code> 745 </div> 746 </form> 747 </div> 748 <div class="info-copy" x-show="quantumMigrationPreview"> 749 <p>Review this batch carefully. Migrated value can only be sent to hybrid address-v1 recipients.</p> 750 <div class="detail-kv"><div class="key">Inputs</div><div x-text="quantumMigrationPreview?.input_count || 0"></div></div> 751 <div class="detail-kv"><div class="key">Transaction size</div><div><span x-text="quantumMigrationPreview?.bytes || 0"></span> bytes</div></div> 752 <div class="detail-kv"><div class="key">Network fee</div><div>IUNA <span x-text="amountLabel(quantumMigrationPreview?.fee || 0)"></span></div></div> 753 <div class="detail-kv"><div class="key">Amount protected</div><div>IUNA <span x-text="amountLabel(quantumMigrationPreview?.amount || 0)"></span></div></div> 754 <div class="detail-kv"><div class="key">Legacy UTXOs after batch</div><div x-text="quantumMigrationPreview?.remaining_legacy_utxos || 0"></div></div> 755 <button class="primary" type="button" @click="submitQuantumMigration" :disabled="quantumMigrationSubmitting" x-text="quantumMigrationSubmitting ? 'Submittingā¦' : 'Confirm migration batch'"></button> 756 </div> 757 </div> 758 <div class="wallet-grid"> 759 <div class="wallet-actions"> 760 <div class="panel"> 761 <h3>Send</h3> 762 <form @submit.prevent="sendTransfer"> 763 <div class="recipient-field"> 764 <label>Recipient<input x-model="transferTo" @input="transferRecipientChanged" autocomplete="off" required></label> 765 <button class="icon-button" type="button" @click="openAddressBookPicker()" title="Choose contact" aria-label="Choose contact"> 766 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H20v18H6.5A2.5 2.5 0 0 1 4 18.5z"></path><path d="M8 7h8"></path><path d="M8 11h6"></path><path d="M8 15h4"></path></svg> 767 </button> 768 </div> 769 <div class="amount-field"> 770 <label>Amount<input x-model="transferAmount" @input="scheduleFeeEstimates" type="number" min="0.000001" step="0.000001" required></label> 771 <button class="amount-max-button" type="button" @click="setMaxTransferAmount" :disabled="transferMaxDisabled()" title="Use maximum spendable amount">Max</button> 772 </div> 773 <label>Fee / byte<input x-model="transferFee" @input="scheduleFeeEstimates" type="number" min="0" step="0.000001" required></label> 774 <div class="fee-preview" :class="{ error: feeEstimateError('transfer') }" x-text="feeEstimateLabel('transfer')" role="status" aria-live="polite"></div> 775 <div class="fee-warning" x-show="feeExceedsAmount('transfer')" x-text="feeExceedsAmountLabel('transfer')" role="status" aria-live="polite"></div> 776 <button class="advanced-toggle" type="button" x-show="!hybridTransferRecipient()" @click="toggleSendAdvanced" x-text="showSendAdvanced ? 'Hide UTXOs' : 'UTXOs'"></button> 777 <div class="send-utxo-summary" x-show="showSendAdvanced && !hybridTransferRecipient()"> 778 <div>Selected UTXOs: <span x-text="selectedTransferUtxos.length"></span></div> 779 <div>Selected total: IUNA <span x-text="amountLabel(selectedTransferUtxoTotal())"></span></div> 780 <div>Required: IUNA <span x-text="amountLabel(transferRequiredTotal())"></span></div> 781 <div class="setup-feedback error" x-show="selectedTransferUtxoShortfall() > 0">Selected UTXOs do not cover amount plus fee</div> 782 <div class="send-utxo-list"> 783 <div class="send-utxo-list-head"> 784 <span>UTXOs</span> 785 <span class="send-utxo-actions"> 786 <button class="utxo-select-button" type="button" @click="selectAllTransferUtxos" :disabled="walletUtxoPage.loading && walletUtxos.length === 0">Select all</button> 787 <button class="utxo-select-button" type="button" @click="clearTransferUtxos" :disabled="selectedTransferUtxos.length === 0">None</button> 788 </span> 789 </div> 790 <template x-for="utxo in walletUtxos" :key="utxoOutpoint(utxo)"> 791 <label class="send-utxo-option" :class="{ disabled: !utxo.spendable }" @click.prevent="toggleTransferUtxoSelection($event, utxo)"> 792 <input type="checkbox" :value="utxoOutpoint(utxo)" :checked="selectedTransferUtxos.includes(utxoOutpoint(utxo))" :disabled="!utxo.spendable"> 793 <span> 794 <span class="utxo-node-label"><span>UTXO</span><span class="utxo-node-amount">IUNA <span x-text="amountLabel(utxo.amount)"></span></span></span> 795 <span class="utxo-status" x-show="!utxo.spendable">Pending</span> 796 <code class="tx-value hash" x-text="utxoOutpoint(utxo)"></code> 797 </span> 798 </label> 799 </template> 800 <div class="dataset-loader" x-show="walletUtxoPage.loading" aria-hidden="true"> 801 <div class="send-utxo-option skeleton-card"><span><span class="skeleton-line medium"></span><span class="skeleton-line long"></span></span></div> 802 <div class="send-utxo-option skeleton-card"><span><span class="skeleton-line short"></span><span class="skeleton-line long"></span></span></div> 803 </div> 804 <div class="page-sentinel" x-show="walletUtxoPage.hasMore" x-init="$nextTick(() => observePageSentinel('walletUtxo', $el))"></div> 805 <div class="tx-modal-empty" x-show="walletUtxos.length === 0 && !walletUtxoPage.loading">No UTXOs</div> 806 </div> 807 </div> 808 <button class="primary" type="submit" :disabled="sendPreparing || sendConfirmBusy" x-text="sendPreparing ? 'Checking...' : 'Send'"></button> 809 </form> 810 </div> 811 <div class="panel"> 812 <div class="panel-head"> 813 <h3>Receive</h3> 814 <button type="button" @click="copyReceiveAddress">Copy</button> 815 </div> 816 <div class="receive-address"> 817 <div class="muted" x-text="status.quantum_migration?.active ? 'Current hybrid Ed25519 + ML-DSA receive address' : 'Legacy Ed25519 address'"></div> 818 <div class="address-box"><code class="wallet-address-link" role="button" tabindex="0" x-text="receiveAddress()" @click="openAddressContact(receiveAddress())" @keydown.enter.prevent="openAddressContact(receiveAddress())" @keydown.space.prevent="openAddressContact(receiveAddress())" title="Add or edit contact"></code></div> 819 <div class="muted" x-show="status.quantum_migration?.active">A new receive address is selected after funds are received. Previous addresses remain monitored by this wallet.</div> 820 <button class="wallet-address-summary" type="button" x-show="fundedWalletAddresses().length > 0" @click="openWalletAddressesModal"> 821 <span x-text="walletAddressSummary()"></span> 822 <span class="wallet-address-summary-action">View details</span> 823 </button> 824 </div> 825 </div> 826 <div class="panel"> 827 <div class="panel-head"> 828 <h3>Address Book</h3> 829 <div class="address-book-actions"> 830 <button class="icon-button" type="button" @click="openAddressBookModal()" title="Add contact" aria-label="Add contact"> 831 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14"></path><path d="M5 12h14"></path></svg> 832 </button> 833 </div> 834 </div> 835 <div class="address-book-list"> 836 <template x-for="entry in addressBookEntries()" :key="entry.address"> 837 <button class="address-book-row" type="button" @click="editAddressBookEntry(entry)" :title="`Edit ${entry.name}`"> 838 <div> 839 <div class="address-book-name" x-text="entry.name"></div> 840 <code class="tx-value hash" x-text="short(entry.address)"></code> 841 </div> 842 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 18l6-6-6-6"></path></svg> 843 </button> 844 </template> 845 <div class="muted" x-show="addressBookEntries().length === 0">No saved addresses</div> 846 </div> 847 </div> 848 </div> 849 <div class="panel wallet-tx-panel"> 850 <div class="panel-head"> 851 <h3>Transactions</h3> 852 <div class="wallet-tx-filters" aria-label="Transaction filters"> 853 <label class="tx-filter" :class="{ active: walletTxFilters.transfer }"> 854 <input type="checkbox" x-model="walletTxFilters.transfer" @change="refreshWalletTransactions()"> 855 <span>Tx</span> 856 </label> 857 <label class="tx-filter" :class="{ active: walletTxFilters.mine }"> 858 <input type="checkbox" x-model="walletTxFilters.mine" @change="refreshWalletTransactions()"> 859 <span>Mine</span> 860 </label> 861 <label class="tx-filter" :class="{ active: walletTxFilters.burn }"> 862 <input type="checkbox" x-model="walletTxFilters.burn" @change="refreshWalletTransactions()"> 863 <span>Burn</span> 864 </label> 865 <label class="tx-filter" :class="{ active: walletTxFilters.reward }"> 866 <input type="checkbox" x-model="walletTxFilters.reward" @change="refreshWalletTransactions()"> 867 <span>Reward</span> 868 </label> 869 </div> 870 </div> 871 <div class="wallet-tx-list"> 872 <template x-for="tx in walletTransactions()" :key="tx.status + '-' + tx.signature"> 873 <div class="wallet-tx-row" :class="{ pending: tx.status === 'pending' }" role="button" tabindex="0" @click="openTransactionModal(tx, { source: 'Wallet' })" @keydown.enter.prevent="openTransactionModal(tx, { source: 'Wallet' })" @keydown.space.prevent="openTransactionModal(tx, { source: 'Wallet' })"> 874 <span class="pill" :class="tx.kind" x-text="tx.direction"></span> 875 <div class="wallet-tx-main"> 876 <div class="tx-field"><span class="tx-label">Amount</span><span class="tx-value money">IUNA <span x-text="amountLabel(tx.amount)"></span></span></div> 877 <div class="tx-field"><span class="tx-label">Fee</span><span class="tx-value money" x-text="txFeeLabel(tx)"></span></div> 878 <div class="tx-field"><span class="tx-label">Status</span><span class="tx-value text" x-text="txTitle(tx)"></span></div> 879 <div class="tx-field"><span class="tx-label">Time</span><span class="tx-value text" x-text="walletTxTimeLabel(tx)"></span></div> 880 <div class="tx-field"><span class="tx-label">From</span><code class="tx-value hash" :class="{ 'wallet-address-link': hasWalletAddress(tx.from) }" role="button" :tabindex="hasWalletAddress(tx.from) ? 0 : -1" x-text="shortAddressLabel(tx.from)" @click.stop="openAddressContact(tx.from)" @keydown.enter.stop.prevent="openAddressContact(tx.from)" @keydown.space.stop.prevent="openAddressContact(tx.from)" :title="hasWalletAddress(tx.from) ? 'Add or edit contact' : null"></code></div> 881 <div class="tx-field" x-show="tx.to"><span class="tx-label">To</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(tx.to)" @click.stop="openAddressContact(tx.to)" @keydown.enter.stop.prevent="openAddressContact(tx.to)" @keydown.space.stop.prevent="openAddressContact(tx.to)" title="Add or edit contact"></code></div> 882 <div class="tx-field" x-show="isMineTx(tx)"><span class="tx-label">Proof Bits</span><span class="tx-value number"><span x-text="txProofBits(tx) ?? '-'"></span> / <span x-text="txDifficultyBits(tx) ?? '-'"></span></span></div> 883 <div class="tx-field" x-show="isMineTx(tx)"><span class="tx-label">Proof Hash</span><code class="tx-value hash" x-text="short(txProofHash(tx))"></code></div> 884 <div class="tx-field"><span class="tx-label">Signature</span><code class="tx-value hash" x-text="short(tx.signature)"></code></div> 885 </div> 886 </div> 887 </template> 888 <div class="dataset-loader" x-show="walletTxPage.loading" aria-hidden="true"> 889 <div class="wallet-tx-row skeleton-card"><div class="wallet-tx-main"><div class="skeleton-line medium"></div><div class="skeleton-line short"></div><div class="skeleton-line long"></div></div></div> 890 <div class="wallet-tx-row skeleton-card"><div class="wallet-tx-main"><div class="skeleton-line short"></div><div class="skeleton-line medium"></div><div class="skeleton-line long"></div></div></div> 891 </div> 892 <div class="page-sentinel" x-show="walletTxPage.hasMore" x-init="$nextTick(() => observePageSentinel('walletTx', $el))"></div> 893 <div class="muted" x-show="walletTransactions().length === 0 && !walletTxPage.loading">No wallet transactions</div> 894 </div> 895 </div> 896 </div> 897 </section> 898 899 <section x-show="tab === 'mining'"> 900 <div class="page-title"> 901 <div class="muted">PoB/VDF block production with PoW issuance actions</div> 902 </div> 903 <div class="mining-grid"> 904 <div class="panel"> 905 <h3>Status</h3> 906 <div class="mine-stats local-mining-stats" aria-label="Local mining status"> 907 <div class="mine-stat"> 908 <div class="mine-stat-label">PoB State</div> 909 <div class="mine-stat-value" x-text="pobStatusLabel()"></div> 910 </div> 911 <div class="mine-stat"> 912 <div class="mine-stat-label">PoB Detail</div> 913 <div class="mine-stat-value" x-text="pobDetailLabel()" :title="pobDetailLabel()"></div> 914 </div> 915 <div class="mine-stat"> 916 <div class="mine-stat-label">PoW State</div> 917 <div class="mine-stat-value" x-text="powStatusShortLabel()"></div> 918 </div> 919 <div class="mine-stat"> 920 <div class="mine-stat-label">Selected Finalizer</div> 921 <code class="mine-stat-value" :class="{ 'wallet-address-link': hasWalletAddress(currentFinalizerAddress()) }" role="button" :tabindex="hasWalletAddress(currentFinalizerAddress()) ? 0 : -1" x-text="currentFinalizerLabel()" @click="openAddressContact(currentFinalizerAddress())" @keydown.enter.prevent="openAddressContact(currentFinalizerAddress())" @keydown.space.prevent="openAddressContact(currentFinalizerAddress())" :title="hasWalletAddress(currentFinalizerAddress()) ? 'Add or edit contact' : null"></code> 922 </div> 923 <div class="mine-stat"> 924 <div class="mine-stat-label">Mempool</div> 925 <div class="mine-stat-value" x-text="localMiningMempoolLabel()"></div> 926 </div> 927 </div> 928 <div class="mining-event-log" x-show="developmentMode()" aria-label="Mining event log"> 929 <div class="mining-event-log-head"><span>Event log</span><span x-text="`${miningEventLog().length} lines`"></span></div> 930 <template x-if="miningEventLog().length === 0"> 931 <div class="mining-event-empty skeleton-card" aria-hidden="true"><div class="skeleton-line"></div></div> 932 </template> 933 <template x-for="event in miningEventLog()" :key="event.key"> 934 <div class="mining-event" :class="event.kind"> 935 <span class="mining-event-dot" aria-hidden="true"></span> 936 <div> 937 <div class="mining-event-title" x-text="event.title"></div> 938 <div class="mining-event-detail" x-text="event.detail"></div> 939 </div> 940 <div class="mining-event-time" x-text="event.time"></div> 941 </div> 942 </template> 943 </div> 944 </div> 945 <div class="panel"> 946 <div class="panel-head mining-head"> 947 <h3>Burn</h3> 948 <label class="toggle-switch" :class="{ active: miningEnabled }"> 949 <input type="checkbox" :checked="miningEnabled" @change="setMiningEnabled($event.target.checked)"> 950 <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span> 951 <span class="toggle-text" x-text="miningEnabled ? 'On' : 'Off'"></span> 952 </label> 953 </div> 954 <div class="panel-description">Burn IUNA to compete for block finalization. Winning burns finalize PoB/VDF blocks and earn the transaction fees in those blocks.</div> 955 <form class="mining-form" @submit.prevent="saveBurn"> 956 <div class="burn-fields"> 957 <label>IUNA per block<input x-model="burnAmountDraft" @input="burnAmountDirty = true; scheduleFeeEstimates()" type="number" min="0.000001" step="0.000001"></label> 958 <label>Fee / byte<input x-model="burnFeeDraft" @input="burnAmountDirty = true; scheduleFeeEstimates()" type="number" min="0" step="0.000001" required></label> 959 <button class="primary" type="submit">Save</button> 960 </div> 961 <div class="fee-preview" x-text="feeEstimateLabel('burn')"></div> 962 <div class="fee-warning" x-show="feeExceedsAmount('burn')" x-text="feeExceedsAmountLabel('burn')" role="status" aria-live="polite"></div> 963 </form> 964 <div class="mine-stats fee-history" aria-label="Recent block fees"> 965 <div class="mine-stat"> 966 <div class="mine-stat-label">Last block fees</div> 967 <div class="mine-stat-value money">IUNA <span x-text="amountLabel(recentBlockFeeAverage(1))"></span></div> 968 </div> 969 <div class="mine-stat"> 970 <div class="mine-stat-label">5 block avg</div> 971 <div class="mine-stat-value money">IUNA <span x-text="amountLabel(recentBlockFeeAverage(5))"></span></div> 972 </div> 973 <div class="mine-stat"> 974 <div class="mine-stat-label">30 block avg</div> 975 <div class="mine-stat-value money">IUNA <span x-text="amountLabel(recentBlockFeeAverage(30))"></span></div> 976 </div> 977 </div> 978 </div> 979 <div class="panel"> 980 <h3>Mine</h3> 981 <div class="panel-description">Search for PoW actions that mint a fixed IUNA reward.</div> 982 <div class="mine-settings-form"> 983 <div class="mine-action-row"> 984 <div class="mine-stats" aria-label="PoW issuance settings"> 985 <div class="mine-stat"> 986 <div class="mine-stat-label">You receive</div> 987 <div class="mine-stat-value money">IUNA <span x-text="amountLabel(powMineReward())"></span></div> 988 </div> 989 <div class="mine-stat"> 990 <div class="mine-stat-label">Finalizer earns</div> 991 <div class="mine-stat-value">IUNA <span x-text="amountLabel(feeEstimates.mine?.fee ?? 0)"></span></div> 992 </div> 993 <div class="mine-stat"> 994 <div class="mine-stat-label">Difficulty <button class="info-button" type="button" @click="openPowDifficultyInfo" title="How difficulty is adjusted" aria-label="How PoW difficulty is adjusted">i</button></div> 995 <div class="mine-stat-value"><span x-text="powDifficultyLabel()"></span> bits</div> 996 </div> 997 </div> 998 <label class="toggle-switch" :class="{ active: powMiningEnabled }" title="Continuously search for PoW mine actions with a small local work budget"> 999 <input type="checkbox" :checked="powMiningEnabled" @change="setPowMiningEnabled($event.target.checked)"> 1000 <span class="toggle-track"><span class="toggle-thumb"></span></span> 1001 <span class="toggle-text" x-text="powMiningEnabled ? 'On' : 'Off'"></span> 1002 </label> 1003 <label class="compact-number-field" title="Local PoW worker count"> 1004 <span>Workers</span> 1005 <input type="number" min="1" :max="maxPowMiningWorkers" :value="powMiningWorkers" @change="setPowMiningWorkers($event.target.value)"> 1006 </label> 1007 </div> 1008 <div class="fee-preview" x-text="autoPowStatusLabel()"></div> 1009 </div> 1010 <div class="panel-separator" x-show="stratumRuntimeEnabled()"></div> 1011 <div class="stratum-config" x-show="stratumRuntimeEnabled()"> 1012 <div class="stratum-note">Use the pool URL below in the miner configuration.</div> 1013 <div class="stratum-fields" aria-label="Stratum settings"> 1014 <div class="stratum-field"> 1015 <div class="stratum-label">Status</div> 1016 <div class="stratum-value" x-text="status.stratum?.enabled ? 'On' : 'Off'"></div> 1017 </div> 1018 <div class="stratum-field"> 1019 <div class="stratum-label">Listener</div> 1020 <code class="stratum-value hash" x-text="stratumListenAddr()"></code> 1021 </div> 1022 <div class="stratum-field"> 1023 <div class="stratum-label">Pool URL</div> 1024 <code class="stratum-value hash" x-text="stratumPoolUrl()"></code> 1025 </div> 1026 </div> 1027 </div> 1028 </div> 1029 </div> 1030 </section> 1031 1032 <section x-show="tab === 'p2p'"> 1033 <div class="panel"> 1034 <div class="peer-toolbar"> 1035 <div> 1036 <h2>Peers</h2> 1037 <div class="panel-description" x-text="p2pAcceptInbound ? 'Manage outbound peers and inspect inbound or outbound sessions. Public node is accepting inbound P2P connections.' : 'Manage outbound peers and inspect sync health. This node is outbound-only and does not open an inbound P2P port.'"></div> 1038 </div> 1039 <form class="peer-form" @submit.prevent="addPeer"> 1040 <label>Peer address<input x-model="peerAddress" placeholder="seed.example:9444"></label> 1041 <button class="primary" type="submit">Add</button> 1042 </form> 1043 </div> 1044 <div class="network-health" x-show="developmentMode()"> 1045 <div class="network-health-state" :class="networkHealthClass()"> 1046 <div class="network-health-label">Network Health</div> 1047 <div class="network-health-value" x-text="networkHealth.state || '-'"></div> 1048 <div class="network-health-detail" x-text="networkHealth.last_chain_payload_error || networkHealth.last_error || 'No peer errors reported'"></div> 1049 </div> 1050 <div class="network-health-grid"> 1051 <div class="peer-summary-item"><div class="peer-summary-label">Local Height</div><div class="peer-summary-value" x-text="networkHealth.local_height ?? '-'"></div></div> 1052 <div class="peer-summary-item"><div class="peer-summary-label">Tip</div><code class="peer-summary-value" x-text="networkTipLabel()"></code></div> 1053 <div class="peer-summary-item"><div class="peer-summary-label">Finalized</div><div class="peer-summary-value" x-text="networkHealth.finalized_height ?? '-'"></div></div> 1054 <div class="peer-summary-item"><div class="peer-summary-label">Finalized Hash</div><code class="peer-summary-value" x-text="short(networkHealth.finalized_hash)"></code></div> 1055 <div class="peer-summary-item"><div class="peer-summary-label">Last Block</div><div class="peer-summary-value" x-text="networkLastBlockAgeLabel()"></div></div> 1056 <div class="peer-summary-item"><div class="peer-summary-label">Best Known</div><div class="peer-summary-value" x-text="networkHealth.best_known_height ?? '-'"></div></div> 1057 <div class="peer-summary-item"><div class="peer-summary-label">Lag</div><div class="peer-summary-value" x-text="networkLagLabel()"></div></div> 1058 <div class="peer-summary-item"><div class="peer-summary-label">Stale</div><div class="peer-summary-value" x-text="networkHealth.stale_peers ?? '-'"></div></div> 1059 <div class="peer-summary-item"><div class="peer-summary-label">Banned</div><div class="peer-summary-value" x-text="networkHealth.banned_peers ?? '-'"></div></div> 1060 <div class="peer-summary-item"><div class="peer-summary-label">Mempool</div><div class="peer-summary-value" x-text="networkHealth.pending_transactions ?? '-'"></div></div> 1061 <div class="peer-summary-item"><div class="peer-summary-label">Plain Tx</div><div class="peer-summary-value" x-text="networkHealth.pending_plain_transactions ?? '-'"></div></div> 1062 <div class="peer-summary-item"><div class="peer-summary-label">V2 Tx</div><div class="peer-summary-value" x-text="networkHealth.pending_v2_transactions ?? '-'"></div></div> 1063 <div class="peer-summary-item"><div class="peer-summary-label">Next Finalizer</div><code class="peer-summary-value" :class="{ 'wallet-address-link': hasWalletAddress(networkHealth.current_leader) }" role="button" :tabindex="hasWalletAddress(networkHealth.current_leader) ? 0 : -1" x-text="networkFinalizerLabel()" @click="openAddressContact(networkHealth.current_leader)" @keydown.enter.prevent="openAddressContact(networkHealth.current_leader)" @keydown.space.prevent="openAddressContact(networkHealth.current_leader)" :title="hasWalletAddress(networkHealth.current_leader) ? 'Add or edit contact' : null"></code></div> 1064 <div class="peer-summary-item"><div class="peer-summary-label">Last Mode</div><div class="peer-summary-value" x-text="networkFinalizerModeLabel()"></div></div> 1065 <div class="peer-summary-item"><div class="peer-summary-label">VDF</div><div class="peer-summary-value" x-text="networkVdfLabel()"></div></div> 1066 <div class="peer-summary-item"><div class="peer-summary-label">Rejected Chain</div><div class="peer-summary-value" x-text="networkHealth.rejected_chain_payloads ?? '-'"></div></div> 1067 <div class="peer-summary-item"><div class="peer-summary-label">Time Offset</div><div class="peer-summary-value" x-text="networkTimeOffsetLabel()"></div></div> 1068 <div class="peer-summary-item"><div class="peer-summary-label">Clock Warnings</div><div class="peer-summary-value" x-text="networkHealth.bad_clock_peers ?? '-'"></div></div> 1069 </div> 1070 </div> 1071 <div class="peer-summary" x-show="developmentMode()"> 1072 <div class="peer-summary-item"><div class="peer-summary-label">Outbound</div><div class="peer-summary-value" x-text="outboundPeers().length"></div></div> 1073 <div class="peer-summary-item"><div class="peer-summary-label">Inbound</div><div class="peer-summary-value" x-text="inboundPeers().length"></div></div> 1074 <div class="peer-summary-item"><div class="peer-summary-label">Healthy</div><div class="peer-summary-value" x-text="healthyPeers().length"></div></div> 1075 <div class="peer-summary-item"><div class="peer-summary-label">Errors</div><div class="peer-summary-value" x-text="failedPeers().length"></div></div> 1076 <div class="peer-summary-item"><div class="peer-summary-label">Shared Height</div><div class="peer-summary-value" x-text="sharedHeightLabel()"></div></div> 1077 </div> 1078 <div class="table-wrap"> 1079 <table class="peer-table"> 1080 <thead><tr><th>Status</th><th>Address</th><th>Direction</th><th>Last Contact</th><th x-show="developmentMode()">Clock</th><th x-show="developmentMode()">Ban</th><th x-show="developmentMode()">Score</th><th>Height</th><th x-show="developmentMode()">Delta</th><th x-show="developmentMode()">Tip</th><th x-show="developmentMode()">Sent</th><th x-show="developmentMode()">Received</th><th x-show="developmentMode()">Last Error</th><th>Actions</th></tr></thead> 1081 <tbody> 1082 <template x-for="peer in peers" :key="peer.address"> 1083 <tr> 1084 <td data-label="Status"><span class="peer-status" :class="peerStatus(peer)" x-text="peerStatusLabel(peer)"></span></td> 1085 <td data-label="Address"><span class="peer-address"><code x-text="peer.address"></code><span class="country-code" x-show="peer.country_code" x-text="peer.country_code"></span></span></td> 1086 <td data-label="Direction" x-text="peer.direction"></td> 1087 <td data-label="Last contact" x-text="peerLastContactLabel(peer)"></td> 1088 <td data-label="Clock" x-show="developmentMode()" x-text="peerClockLabel(peer)"></td> 1089 <td data-label="Ban" x-show="developmentMode()" x-text="peerBanLabel(peer)"></td> 1090 <td data-label="Score" x-show="developmentMode()" x-text="peer.misbehavior_score ?? 0"></td> 1091 <td data-label="Height" x-text="peer.last_known_height ?? '-'"></td> 1092 <td data-label="Delta" x-show="developmentMode()" x-text="peerHeightDelta(peer)"></td> 1093 <td data-label="Tip" x-show="developmentMode()"><code x-text="short(peer.last_known_tip_hash)"></code></td> 1094 <td data-label="Sent" x-show="developmentMode()" x-text="peer.messages_sent"></td> 1095 <td data-label="Received" x-show="developmentMode()" x-text="peer.messages_received"></td> 1096 <td data-label="Last error" x-show="developmentMode()" x-text="peer.last_error || ''"></td> 1097 <td data-label="Actions"><div class="peer-actions"><button class="peer-details" type="button" @click="openPeerModal(peer)">Details</button><button class="peer-remove" type="button" x-show="canRemovePeer(peer)" @click="removePeer(peer)">Remove</button><span class="muted" x-show="!canRemovePeer(peer)">Observed</span></div></td> 1098 </tr> 1099 </template> 1100 <tr class="skeleton-card" x-show="peerPage.loading" aria-hidden="true"> 1101 <td colspan="14"><div class="skeleton-table-cell"></div></td> 1102 </tr> 1103 <tr class="skeleton-card" x-show="peerPage.loading" aria-hidden="true"> 1104 <td colspan="14"><div class="skeleton-table-cell"></div></td> 1105 </tr> 1106 <tr x-show="peerPage.hasMore"><td colspan="14"><div class="page-sentinel" x-init="$nextTick(() => observePageSentinel('peer', $el))"></div></td></tr> 1107 <tr x-show="peers.length === 0 && !peerPage.loading"><td colspan="14">No peers</td></tr> 1108 </tbody> 1109 </table> 1110 </div> 1111 </div> 1112 <div class="panel" x-show="developmentMode()"> 1113 <h2>Metrics</h2> 1114 <div class="grid"> 1115 <div class="metric"><div class="label">Inbound Sessions</div><div class="value" x-text="p2pMetrics.inbound_sessions_started ?? 0"></div></div> 1116 <div class="metric"><div class="label">Inbound Rejects</div><div class="value" x-text="p2pMetrics.inbound_sessions_rejected ?? 0"></div></div> 1117 <div class="metric"><div class="label">Outbound Attempts</div><div class="value" x-text="p2pMetrics.outbound_connect_attempts ?? 0"></div></div> 1118 <div class="metric"><div class="label">Connect Failures</div><div class="value" x-text="p2pMetrics.outbound_connect_failures ?? 0"></div></div> 1119 <div class="metric"><div class="label">Session Failures</div><div class="value" x-text="p2pMetrics.session_failures ?? 0"></div></div> 1120 <div class="metric"><div class="label">Parse Errors</div><div class="value" x-text="p2pMetrics.parse_errors ?? 0"></div></div> 1121 <div class="metric"><div class="label">Empty Frames</div><div class="value" x-text="p2pMetrics.empty_frames ?? 0"></div></div> 1122 <div class="metric"><div class="label">Self Rejects</div><div class="value" x-text="p2pMetrics.self_peer_rejections ?? 0"></div></div> 1123 <div class="metric"><div class="label">Self Skips</div><div class="value" x-text="p2pMetrics.self_peer_skips ?? 0"></div></div> 1124 <div class="metric"><div class="label">Received</div><div class="value" x-text="p2pMetrics.envelopes_received ?? 0"></div></div> 1125 <div class="metric"><div class="label">Bytes In</div><div class="value" x-text="p2pMetrics.bytes_received ?? 0"></div></div> 1126 <div class="metric"><div class="label">Status Rx</div><div class="value" x-text="p2pMetrics.peer_status_envelopes_received ?? 0"></div></div> 1127 <div class="metric"><div class="label">Hello Rx</div><div class="value" x-text="p2pMetrics.hello_envelopes_received ?? 0"></div></div> 1128 <div class="metric"><div class="label">Inventory Rx</div><div class="value" x-text="p2pMetrics.inventory_envelopes_received ?? 0"></div></div> 1129 <div class="metric"><div class="label">Data Rx</div><div class="value" x-text="p2pMetrics.data_envelopes_received ?? 0"></div></div> 1130 <div class="metric"><div class="label">Tx Rx</div><div class="value" x-text="p2pMetrics.transactions_received ?? 0"></div></div> 1131 <div class="metric"><div class="label">Tx Envelopes Rx</div><div class="value" x-text="p2pMetrics.transaction_envelopes_received ?? 0"></div></div> 1132 <div class="metric"><div class="label">Burn Bundles Rx</div><div class="value" x-text="p2pMetrics.burn_bundles_received ?? 0"></div></div> 1133 <div class="metric"><div class="label">Burn Bundle Envelopes Rx</div><div class="value" x-text="p2pMetrics.burn_bundle_envelopes_received ?? 0"></div></div> 1134 <div class="metric"><div class="label">Control Rx</div><div class="value" x-text="p2pMetrics.control_envelopes_received ?? 0"></div></div> 1135 </div> 1136 <div class="metric-context"> 1137 <div class="tx-field"><span class="tx-label">Last Failure</span><span class="tx-value text" x-text="p2pMetrics.last_session_failure || '-'"></span></div> 1138 <div class="tx-field"><span class="tx-label">Last Empty</span><span class="tx-value text" x-text="p2pMetrics.last_empty_frame_remote || '-'"></span></div> 1139 <div class="tx-field"><span class="tx-label">Last Parse</span><span class="tx-value text" x-text="p2pMetrics.last_parse_error || '-'"></span></div> 1140 </div> 1141 </div> 1142 </section> 1143 1144 <section x-show="tab === 'chain'"> 1145 <div class="explorer-shell"> 1146 <div class="block-rail-wrap"> 1147 <div class="block-rail-head"> 1148 <h2>Blocks</h2> 1149 <div class="muted"><span x-text="blocks.length"></span> loaded</div> 1150 </div> 1151 <div class="block-rail" x-ref="blockRail" @scroll.debounce.200ms="maybeLoadOlderBlocks($event)"> 1152 <template x-for="block in blocks" :key="block.hash"> 1153 <button class="block-card" :class="{ selected: selectedBlock?.hash === block.hash, 'new-block': newBlockHashes.has(block.hash) }" @click="selectBlock(block)" type="button" title="Open block details"> 1154 <div class="block-card-select"> 1155 <div class="block-height" x-text="block.height"></div> 1156 <div class="block-meta"> 1157 <span x-text="burnCountLabel(block)"></span> 1158 <span x-text="transferCountLabel(block)"></span> 1159 <span x-text="mineCountLabel(block)"></span> 1160 </div> 1161 </div> 1162 <div class="block-miner" x-text="blockFinalizerLabel(block)"></div> 1163 </button> 1164 </template> 1165 <template x-if="loadingInitialBlocks && blocks.length === 0"> 1166 <div class="block-skeleton-group" aria-hidden="true"> 1167 <div class="block-card block-card-skeleton skeleton-card"> 1168 <div class="skeleton-block-height"></div> 1169 <div class="skeleton-block-meta"><span class="skeleton-pill wide"></span><span class="skeleton-pill"></span><span class="skeleton-pill wide"></span></div> 1170 <div class="skeleton-block-miner"></div> 1171 </div> 1172 <div class="block-card block-card-skeleton skeleton-card"> 1173 <div class="skeleton-block-height"></div> 1174 <div class="skeleton-block-meta"><span class="skeleton-pill"></span><span class="skeleton-pill wide"></span><span class="skeleton-pill"></span></div> 1175 <div class="skeleton-block-miner"></div> 1176 </div> 1177 <div class="block-card block-card-skeleton skeleton-card"> 1178 <div class="skeleton-block-height"></div> 1179 <div class="skeleton-block-meta"><span class="skeleton-pill wide"></span><span class="skeleton-pill wide"></span><span class="skeleton-pill"></span></div> 1180 <div class="skeleton-block-miner"></div> 1181 </div> 1182 <div class="block-card block-card-skeleton skeleton-card"> 1183 <div class="skeleton-block-height"></div> 1184 <div class="skeleton-block-meta"><span class="skeleton-pill"></span><span class="skeleton-pill"></span><span class="skeleton-pill wide"></span></div> 1185 <div class="skeleton-block-miner"></div> 1186 </div> 1187 </div> 1188 </template> 1189 <template x-if="loadingOlder"> 1190 <div class="block-card block-card-skeleton skeleton-card" aria-hidden="true"> 1191 <div class="skeleton-block-height"></div> 1192 <div class="skeleton-block-meta"><span class="skeleton-pill wide"></span><span class="skeleton-pill"></span><span class="skeleton-pill wide"></span></div> 1193 <div class="skeleton-block-miner"></div> 1194 </div> 1195 </template> 1196 <div class="page-sentinel block-page-sentinel" x-show="hasMoreBlocks" x-init="$nextTick(() => observeBlockSentinel($el))"></div> 1197 </div> 1198 </div> 1199 1200 <section class="panel"> 1201 <h2>Block Detail</h2> 1202 <template x-if="selectedBlock"> 1203 <div class="detail-grid"> 1204 <div> 1205 <div class="detail-kv"><div class="key">Height</div><div x-text="selectedBlock.height"></div></div> 1206 <div class="detail-kv"><div class="key">Time</div><div x-text="blockTimestampLabel(selectedBlock)"></div></div> 1207 <div class="detail-kv"><div class="key">Hash</div><code x-text="selectedBlock.hash"></code></div> 1208 <div class="detail-kv"><div class="key">Previous</div><code x-text="short(selectedBlock.prev_hash)"></code></div> 1209 <div class="detail-kv"> 1210 <div class="key">Finalizer</div> 1211 <button class="detail-link" type="button" @click="openBurnLeaderRanksModal(selectedBlock)" title="Burn leader ranks"> 1212 <code x-text="shortAddressLabel(selectedBlock.miner)"></code> 1213 </button> 1214 </div> 1215 <div class="detail-kv"><div class="key">Mode</div><div x-text="selectedBlock.finalizer_mode === 'recovery' ? 'Recovery' : `Rank ${selectedBlock.finalizer_rank ?? 0}`"></div></div> 1216 <div class="detail-kv"> 1217 <div class="key">Reward</div> 1218 <button class="detail-link" type="button" @click="openBlockRewardModal(selectedBlock)" title="Show paid and distributed fees" aria-label="Show block reward fee flow"> 1219 <span class="link-value">IUNA <span x-text="amountLabel(selectedBlock.reward)"></span></span> 1220 </button> 1221 </div> 1222 <div class="detail-kv"> 1223 <div class="key">Burn Bundles</div> 1224 <button class="detail-link" type="button" @click="openBurnBundleModal(selectedBlock)" title="How the burn bundle attestations were selected" aria-label="Show burn bundle selection details"> 1225 <span class="link-value" x-text="blockBurnBundleRatio(selectedBlock)"></span> 1226 </button> 1227 </div> 1228 <div class="detail-kv"><div class="key">Burns</div><div x-text="blockBurnCount(selectedBlock)"></div></div> 1229 <div class="detail-kv"><div class="key">Transfers</div><div x-text="blockTransferCount(selectedBlock)"></div></div> 1230 <div class="detail-kv"><div class="key">Total Lost</div><div>IUNA <span x-text="amountLabel(blockLostIuna(selectedBlock))"></span></div></div> 1231 <div class="detail-kv"> 1232 <div class="key">Storage</div> 1233 <button class="detail-link" type="button" @click="openBlockBytesModal(selectedBlock)" title="Compact storage byte breakdown"> 1234 <span x-text="blockTotalBytes(selectedBlock)"></span>B 1235 </button> 1236 </div> 1237 <div class="detail-kv"><div class="key">VDF</div><div><span x-text="selectedBlock.vdf_rounds"></span> rounds</div></div> 1238 </div> 1239 <div class="tx-list"> 1240 <h3>Transactions</h3> 1241 <div class="tx-section"> 1242 <div class="tx-section-title"><span>Envelope</span><span class="tx-section-meta wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(selectedBlock.miner)" @click.stop="openAddressContact(selectedBlock.miner)" @keydown.enter.stop.prevent="openAddressContact(selectedBlock.miner)" @keydown.space.stop.prevent="openAddressContact(selectedBlock.miner)" title="Add or edit contact"></span></div> 1243 <div class="tx-scroll-list"> 1244 <template x-for="tx in selectedBlock.transactions" :key="tx.signature"> 1245 <div class="tx-card" role="button" tabindex="0" @click="openTransactionModal(tx, { source: 'Envelope', blockHeight: selectedBlock.height, blockFinalizer: selectedBlock.miner })" @keydown.enter.prevent="openTransactionModal(tx, { source: 'Envelope', blockHeight: selectedBlock.height, blockFinalizer: selectedBlock.miner })" @keydown.space.prevent="openTransactionModal(tx, { source: 'Envelope', blockHeight: selectedBlock.height, blockFinalizer: selectedBlock.miner })"> 1246 <span class="pill" :class="txPillClass(tx)" x-text="txPillLabel(tx)"></span> 1247 <div class="tx-field"><span class="tx-label">Amount</span><span class="tx-value money">IUNA <span x-text="amountLabel(txAmount(tx))"></span></span></div> 1248 <div class="tx-field"><span class="tx-label">Fee</span><span class="tx-value money" x-text="txFeeLabel(tx)"></span></div> 1249 <div class="tx-field"><span class="tx-label">From</span><code class="tx-value hash" :class="{ 'wallet-address-link': hasWalletAddress(txFrom(tx)) }" role="button" :tabindex="hasWalletAddress(txFrom(tx)) ? 0 : -1" x-text="shortAddressLabel(txFrom(tx))" @click.stop="openAddressContact(txFrom(tx))" @keydown.enter.stop.prevent="openAddressContact(txFrom(tx))" @keydown.space.stop.prevent="openAddressContact(txFrom(tx))" :title="hasWalletAddress(txFrom(tx)) ? 'Add or edit contact' : null"></code></div> 1250 <div class="tx-field" x-show="txTo(tx)"><span class="tx-label">To</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(txTo(tx))" @click.stop="openAddressContact(txTo(tx))" @keydown.enter.stop.prevent="openAddressContact(txTo(tx))" @keydown.space.stop.prevent="openAddressContact(txTo(tx))" title="Add or edit contact"></code></div> 1251 <div class="tx-field" x-show="isMineTx(tx)"><span class="tx-label">Proof Bits</span><span class="tx-value number"><span x-text="txProofBits(tx) ?? '-'"></span> / <span x-text="txDifficultyBits(tx) ?? '-'"></span></span></div> 1252 <div class="tx-field" x-show="isMineTx(tx)"><span class="tx-label">Proof Hash</span><code class="tx-value hash" x-text="short(txProofHash(tx))"></code></div> 1253 <div class="tx-field"><span class="tx-label">Signature</span><code class="tx-value hash" x-text="short(tx.signature)"></code></div> 1254 </div> 1255 </template> 1256 </div> 1257 <div class="muted" x-show="selectedBlock.transactions.length === 0">No envelope transactions</div> 1258 </div> 1259 <template x-for="bundle in selectedBlock.burn_bundles || selectedBlock.burnBundles || []" :key="bundle.hash"> 1260 <details class="tx-section"> 1261 <summary class="tx-section-title"><span x-text="`Burn bundle ${bundle.slot}`"></span><span class="tx-section-meta"><span class="wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(bundle.member)" @click.stop="openAddressContact(bundle.member)" @keydown.enter.stop.prevent="openAddressContact(bundle.member)" @keydown.space.stop.prevent="openAddressContact(bundle.member)" title="Add or edit contact"></span> Ā· <span x-text="bundle.byte_size || bundle.byteSize || 0"></span>B</span></summary> 1262 <template x-for="tx in bundle.burns" :key="tx.signature"> 1263 <div class="tx-card" role="button" tabindex="0" @click="openTransactionModal(tx, { source: 'Burn bundle', blockHeight: selectedBlock.height, blockFinalizer: bundle.member })" @keydown.enter.prevent="openTransactionModal(tx, { source: 'Burn bundle', blockHeight: selectedBlock.height, blockFinalizer: bundle.member })" @keydown.space.prevent="openTransactionModal(tx, { source: 'Burn bundle', blockHeight: selectedBlock.height, blockFinalizer: bundle.member })"> 1264 <span class="pill" :class="txPillClass(tx)" x-text="txPillLabel(tx)"></span> 1265 <div class="tx-field"><span class="tx-label">Amount</span><span class="tx-value money">IUNA <span x-text="amountLabel(txAmount(tx))"></span></span></div> 1266 <div class="tx-field"><span class="tx-label">Fee</span><span class="tx-value money" x-text="txFeeLabel(tx)"></span></div> 1267 <div class="tx-field"><span class="tx-label">From</span><code class="tx-value hash" :class="{ 'wallet-address-link': hasWalletAddress(txFrom(tx)) }" role="button" :tabindex="hasWalletAddress(txFrom(tx)) ? 0 : -1" x-text="shortAddressLabel(txFrom(tx))" @click.stop="openAddressContact(txFrom(tx))" @keydown.enter.stop.prevent="openAddressContact(txFrom(tx))" @keydown.space.stop.prevent="openAddressContact(txFrom(tx))" :title="hasWalletAddress(txFrom(tx)) ? 'Add or edit contact' : null"></code></div> 1268 <div class="tx-field" x-show="txTo(tx)"><span class="tx-label">To</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(txTo(tx))" @click.stop="openAddressContact(txTo(tx))" @keydown.enter.stop.prevent="openAddressContact(txTo(tx))" @keydown.space.stop.prevent="openAddressContact(txTo(tx))" title="Add or edit contact"></code></div> 1269 <div class="tx-field"><span class="tx-label">Signature</span><code class="tx-value hash" x-text="short(tx.signature)"></code></div> 1270 </div> 1271 </template> 1272 <div class="muted" x-show="bundle.burns.length === 0">No burns in bundle</div> 1273 </details> 1274 </template> 1275 <div class="muted" x-show="selectedBlock.transactions.length === 0 && !(selectedBlock.burn_bundles || selectedBlock.burnBundles || []).length">No transactions</div> 1276 </div> 1277 </div> 1278 </template> 1279 <div class="muted" x-show="!selectedBlock">Select a block</div> 1280 </section> 1281 1282 <section class="panel mempool-panel" x-show="mempool.length > 0 || mempoolPage.loading"> 1283 <h2>Mempool</h2> 1284 <div class="mempool-strip"> 1285 <template x-for="tx in mempool" :key="tx.signature"> 1286 <div class="mempool-item" :class="mempoolItemClass(tx)" role="button" tabindex="0" @click="openTransactionModal(tx, { source: 'Mempool' })" @keydown.enter.prevent="openTransactionModal(tx, { source: 'Mempool' })" @keydown.space.prevent="openTransactionModal(tx, { source: 'Mempool' })"> 1287 <div class="mempool-top"> 1288 <div class="mempool-top-meta"> 1289 <div class="mempool-state" x-show="mempoolItemClass(tx).includes('new-since-block')">New since last block</div> 1290 <div class="mempool-time" x-show="mempoolSeenTimeLabel(tx)" x-text="mempoolSeenTimeLabel(tx)"></div> 1291 </div> 1292 <span class="pill" :class="tx.kind" x-text="tx.kind"></span> 1293 </div> 1294 <div class="tx-field"><span class="tx-label">Amount</span><span class="tx-value money">IUNA <span x-text="amountLabel(txAmount(tx))"></span></span></div> 1295 <div class="tx-field"><span class="tx-label">Fee</span><span class="tx-value money" x-text="txFeeLabel(tx)"></span></div> 1296 <div class="tx-field"><span class="tx-label">From</span><code class="tx-value hash" :class="{ 'wallet-address-link': hasWalletAddress(txFrom(tx)) }" role="button" :tabindex="hasWalletAddress(txFrom(tx)) ? 0 : -1" x-text="shortAddressLabel(txFrom(tx))" @click.stop="openAddressContact(txFrom(tx))" @keydown.enter.stop.prevent="openAddressContact(txFrom(tx))" @keydown.space.stop.prevent="openAddressContact(txFrom(tx))" :title="hasWalletAddress(txFrom(tx)) ? 'Add or edit contact' : null"></code></div> 1297 <div class="tx-field" x-show="txTo(tx)"><span class="tx-label">To</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(txTo(tx))" @click.stop="openAddressContact(txTo(tx))" @keydown.enter.stop.prevent="openAddressContact(txTo(tx))" @keydown.space.stop.prevent="openAddressContact(txTo(tx))" title="Add or edit contact"></code></div> 1298 <div class="tx-field" x-show="isMineTx(tx)"><span class="tx-label">Proof Bits</span><span class="tx-value number"><span x-text="txProofBits(tx) ?? '-'"></span> / <span x-text="txDifficultyBits(tx) ?? '-'"></span></span></div> 1299 <div class="tx-field" x-show="isMineTx(tx)"><span class="tx-label">Proof Hash</span><code class="tx-value hash" x-text="short(txProofHash(tx))"></code></div> 1300 <div class="tx-field"><span class="tx-label">Signature</span><code class="tx-value hash" x-text="short(tx.signature)"></code></div> 1301 </div> 1302 </template> 1303 <template x-if="mempoolPage.loading"> 1304 <div class="mempool-item skeleton-card" aria-hidden="true"> 1305 <div class="skeleton-line short"></div> 1306 <div class="skeleton-line medium"></div> 1307 <div class="skeleton-line long"></div> 1308 </div> 1309 </template> 1310 <div class="page-sentinel" x-show="mempoolPage.hasMore" x-init="$nextTick(() => observePageSentinel('mempool', $el))"></div> 1311 </div> 1312 </section> 1313 </div> 1314 </section> 1315 <section x-show="tab === 'metrics'"> 1316 <div class="metrics-shell"> 1317 <div class="metrics-head"> 1318 <div class="segmented metrics-range" role="group" aria-label="Metrics block range"> 1319 <button type="button" :class="{ active: metricsRange === 100 }" @click="setMetricsRange(100)">Last 100</button> 1320 <button type="button" :class="{ active: metricsRange === 1000 }" @click="setMetricsRange(1000)">Last 1000</button> 1321 <button type="button" :class="{ active: metricsRange === 'all' }" @click="setMetricsRange('all')">All</button> 1322 </div> 1323 </div> 1324 <div class="metrics-summary"> 1325 <div class="metric"><div class="label">Latest block</div><div class="value" x-text="metricsLatest().height ?? '-'"></div></div> 1326 <div class="metric"><div class="label">Supply</div><div class="value" x-text="metricAmountLabel(metricsLatest().circulatingSupply)"></div></div> 1327 <div class="metric"><div class="label">Transactions in block</div><div class="value" x-text="metricsLatest().transactionCount ?? '-'"></div></div> 1328 <div class="metric"><div class="label">Total UTXOs</div><div class="value" x-text="metricsLatest().utxoCount ?? '-'"></div></div> 1329 <div class="metric"><div class="label">Total burned</div><div class="value" x-text="metricAmountLabel(metricsLatest().totalBurnedAmount)"></div></div> 1330 <div class="metric"><div class="label">Difficulty</div><div class="value" x-text="metricsLatest().mineDifficultyBits ?? '-'"></div></div> 1331 </div> 1332 <div class="metrics-grid" x-show="(loadingMetrics || metricsPreparing()) && metricsCharts().length === 0"> 1333 <article class="metric-chart-card skeleton-card" aria-hidden="true"> 1334 <div class="metric-chart-head"><div class="skeleton-line medium"></div><div class="skeleton-line short"></div></div> 1335 <div class="metric-chart-frame"><div class="skeleton-line long"></div></div> 1336 </article> 1337 <article class="metric-chart-card skeleton-card" aria-hidden="true"> 1338 <div class="metric-chart-head"><div class="skeleton-line short"></div><div class="skeleton-line medium"></div></div> 1339 <div class="metric-chart-frame"><div class="skeleton-line long"></div></div> 1340 </article> 1341 </div> 1342 <div class="metrics-empty" x-show="metricsPreparing() && !loadingMetrics">Preparing development mode from the current chain. Metrics will stay updated in the background.</div> 1343 <div class="metrics-empty" x-show="metricsCharts().length === 0 && !loadingMetrics && !metricsPreparing()">No metrics collected yet</div> 1344 <div class="metrics-grid"> 1345 <template x-for="chart in metricsCharts()" :key="chart.id"> 1346 <article class="metric-chart-card"> 1347 <div class="metric-chart-head"> 1348 <h3 class="metric-chart-title" x-text="chart.title"></h3> 1349 <div class="metric-chart-value" x-text="metricLatestValueLabel(chart)"></div> 1350 </div> 1351 <div class="metric-chart-frame"> 1352 <div class="metric-chart-y-axis"> 1353 <template x-for="tick in metricYAxisTicks(chart)" :key="`${chart.id}-y-${tick}`"> 1354 <span class="metric-chart-axis-label" :style="metricYAxisLabelStyle(chart, tick)" x-text="metricAxisValueLabel(chart, tick)"></span> 1355 </template> 1356 </div> 1357 <div class="metric-chart-plot" @mousemove="setMetricHoverFromPlot(chart, $event)" @mouseleave="clearMetricHover(chart)"> 1358 <svg class="metric-chart-svg" viewBox="0 0 300 148" preserveAspectRatio="none" role="img" :aria-label="chart.title"> 1359 <path class="metric-chart-gridline" :d="metricGridPath(chart)"></path> 1360 <line class="metric-chart-axis" x1="4" y1="8" x2="4" y2="132"></line> 1361 <line class="metric-chart-axis" x1="4" y1="132" x2="296" y2="132"></line> 1362 <polyline class="metric-chart-line" :points="metricChartPoints(chart)"></polyline> 1363 </svg> 1364 <template x-if="metricHover?.chartId === chart.id"> 1365 <div class="metric-chart-hover-point" :style="metricHoverPointStyle(chart)" :title="metricTooltipLabel(chart)"></div> 1366 </template> 1367 <template x-if="metricHover?.chartId === chart.id"> 1368 <div class="metric-chart-tooltip" :style="metricTooltipStyle(chart)" x-text="metricTooltipLabel(chart)"></div> 1369 </template> 1370 </div> 1371 <div class="metric-chart-x-axis"> 1372 <template x-for="tick in metricXAxisTicks(chart)" :key="`${chart.id}-x-${tick}`"> 1373 <span class="metric-chart-axis-label" :style="metricXAxisLabelStyle(chart, tick)" x-text="`#${tick}`"></span> 1374 </template> 1375 </div> 1376 </div> 1377 </article> 1378 </template> 1379 </div> 1380 </div> 1381 </section> 1382 <section x-show="developmentMode() && tab === 'leaderboards'"> 1383 <div class="leaderboard-grid"> 1384 <article class="leaderboard-card"> 1385 <div class="leaderboard-tabs" role="tablist" aria-label="Leaderboard"> 1386 <button type="button" role="tab" :aria-selected="leaderboardTab === 'balances'" :class="{ active: leaderboardTab === 'balances' }" @click="leaderboardTab = 'balances'">Balance</button> 1387 <button type="button" role="tab" :aria-selected="leaderboardTab === 'miners'" :class="{ active: leaderboardTab === 'miners' }" @click="leaderboardTab = 'miners'">Miners</button> 1388 <button type="button" role="tab" :aria-selected="leaderboardTab === 'burners'" :class="{ active: leaderboardTab === 'burners' }" @click="leaderboardTab = 'burners'">Burners</button> 1389 <button type="button" role="tab" :aria-selected="leaderboardTab === 'mineProofs'" :class="{ active: leaderboardTab === 'mineProofs' }" @click="leaderboardTab = 'mineProofs'">Proof bits</button> 1390 </div> 1391 <template x-if="leaderboardTab !== 'mineProofs'"> 1392 <div> 1393 <h3 x-text="leaderboardTitle(leaderboardTab)"></h3> 1394 <div class="leaderboard-list"> 1395 <template x-for="(row, index) in leaderboardRows(leaderboardTab)" :key="`${leaderboardTab}-${row.address}`"> 1396 <div class="leaderboard-row"> 1397 <div class="leaderboard-rank" :class="leaderboardRankClass(index)" x-text="leaderboardRankLabel(index)"></div> 1398 <div class="leaderboard-main"> 1399 <code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(row.address)" @click="openAddressContact(row.address)" @keydown.enter.prevent="openAddressContact(row.address)" @keydown.space.prevent="openAddressContact(row.address)" title="Add or edit contact"></code> 1400 <div class="muted" x-text="leaderboardCountLabel(leaderboardTab, row)"></div> 1401 </div> 1402 <div class="leaderboard-amount" x-text="leaderboardAmountLabel(row)"></div> 1403 </div> 1404 </template> 1405 <template x-for="index in leaderboardPlaceholderRanks(leaderboardRows(leaderboardTab).length)" :key="`${leaderboardTab}-empty-${index}`"> 1406 <div class="leaderboard-row leaderboard-row-empty" aria-hidden="true"> 1407 <div class="leaderboard-rank" x-text="`#${index + 1}`"></div> 1408 <div class="leaderboard-main"> </div> 1409 <div class="leaderboard-amount"> </div> 1410 </div> 1411 </template> 1412 </div> 1413 </div> 1414 </template> 1415 <template x-if="leaderboardTab === 'mineProofs'"> 1416 <div> 1417 <h3>Top 10 Mine Proof Bits</h3> 1418 <div class="leaderboard-list"> 1419 <template x-for="(row, index) in topMineProofRows()" :key="`${row.proofHash}-${row.height}`"> 1420 <div class="leaderboard-row"> 1421 <div class="leaderboard-rank" :class="leaderboardRankClass(index)" x-text="leaderboardRankLabel(index)"></div> 1422 <div class="leaderboard-main"> 1423 <code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(row.address)" @click="openAddressContact(row.address)" @keydown.enter.prevent="openAddressContact(row.address)" @keydown.space.prevent="openAddressContact(row.address)" title="Add or edit contact"></code> 1424 <div class="muted">Block #<span x-text="row.height"></span></div> 1425 </div> 1426 <div class="leaderboard-amount"><span x-text="row.proofBits"></span> bits</div> 1427 </div> 1428 </template> 1429 <template x-for="index in leaderboardPlaceholderRanks(topMineProofRows().length)" :key="`mine-proofs-empty-${index}`"> 1430 <div class="leaderboard-row leaderboard-row-empty" aria-hidden="true"> 1431 <div class="leaderboard-rank" x-text="`#${index + 1}`"></div> 1432 <div class="leaderboard-main"> </div> 1433 <div class="leaderboard-amount"> </div> 1434 </div> 1435 </template> 1436 </div> 1437 </div> 1438 </template> 1439 </article> 1440 </div> 1441 </section> 1442 <section x-show="tab === 'settings'"> 1443 <div class="settings-grid"> 1444 <div class="panel"> 1445 <div class="panel-head"> 1446 <h2>Settings</h2> 1447 <span class="pill" x-text="advancedMode() ? 'Node mode' : 'Wallet mode'"></span> 1448 </div> 1449 <div class="settings-mode-row"> 1450 <div class="settings-mode-copy"> 1451 <div class="settings-mode-title">Mode</div> 1452 <div class="muted" x-text="advancedMode() ? 'Node mode shows mining and peer controls.' : 'Wallet mode keeps the interface focused on wallet and chain views.'"></div> 1453 </div> 1454 <label class="toggle-switch" :class="{ active: advancedMode() }"> 1455 <input type="checkbox" :checked="advancedMode()" @change="setUiMode($event.target.checked ? 'advanced' : 'basic')"> 1456 <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span> 1457 <span class="toggle-text" x-text="advancedMode() ? 'Node' : 'Wallet'"></span> 1458 </label> 1459 </div> 1460 </div> 1461 <div class="panel" x-show="advancedMode()"> 1462 <div class="settings-mode-row"> 1463 <div class="settings-mode-copy"> 1464 <div class="settings-mode-title">Fallback VDF</div> 1465 <div class="muted">Top <span x-text="recoveryVdfTopRankPercent"></span>% of ticket ranks run fallback work. Recovery remains available to every node.</div> 1466 </div> 1467 <label>Top ranks 1468 <input type="range" min="0" max="100" step="5" :value="recoveryVdfTopRankPercent" @change="setRecoveryVdfTopRankPercent($event.target.value)"> 1469 </label> 1470 </div> 1471 </div> 1472 <div class="panel" x-show="advancedMode()"> 1473 <h3>Node Networking</h3> 1474 <div class="settings-mode-row"> 1475 <div class="settings-mode-copy"> 1476 <div class="settings-mode-title">Public node</div> 1477 <div class="muted" x-text="p2pAcceptInbound ? 'Accepting inbound P2P connections.' : 'Outbound-only P2P; no inbound port is open.'"></div> 1478 </div> 1479 <label class="toggle-switch" :class="{ active: p2pAcceptInbound }"> 1480 <input type="checkbox" :checked="p2pAcceptInbound" @change="setP2pAcceptInbound($event.target.checked)"> 1481 <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span> 1482 <span class="toggle-text" x-text="p2pAcceptInbound ? 'Public' : 'Private'"></span> 1483 </label> 1484 </div> 1485 <form class="settings-form public-p2p-form" x-show="p2pAcceptInbound" x-transition @submit.prevent="saveP2pAnnounce"> 1486 <label>Bind port<input x-model.number="p2pBindPort" @input="p2pBindPortDirty = true" type="number" min="1" max="65535" step="1" required></label> 1487 <label>Public P2P address<input x-model="p2pAnnounceAddr" @input="p2pAnnounceDirty = true" placeholder="203.0.113.10:9444"></label> 1488 <div class="muted">Use this only when TCP port <span x-text="p2pBindPort"></span> is reachable from the internet.</div> 1489 <div class="setup-actions"><button class="primary" type="submit">Save</button></div> 1490 </form> 1491 </div> 1492 <div class="panel" x-show="advancedMode() && p2pAcceptInbound"> 1493 <div class="settings-mode-row"> 1494 <div class="settings-mode-copy"> 1495 <div class="settings-mode-title">Wallet endpoint</div> 1496 <div class="muted" x-text="walletEndpointEnabled ? 'Public read and transaction relay API for lightweight wallets.' : 'Public wallet API is disabled.'"></div> 1497 </div> 1498 <label class="toggle-switch" :class="{ active: walletEndpointEnabled }"> 1499 <input type="checkbox" :checked="walletEndpointEnabled" @change="setWalletEndpointEnabled($event.target.checked)"> 1500 <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span> 1501 <span class="toggle-text" x-text="walletEndpointEnabled ? 'On' : 'Off'"></span> 1502 </label> 1503 </div> 1504 <form class="settings-form" x-show="walletEndpointEnabled" x-transition @submit.prevent="saveWalletEndpointSettings"> 1505 <label>Wallet API port<input x-model.number="walletEndpointBindPort" @input="walletEndpointBindPortDirty = true" type="number" min="1" max="65535" step="1" required></label> 1506 <div class="muted">This is a separate listener from the management UI. Only forward this port publicly.</div> 1507 <div class="setup-actions"><button class="primary" type="submit">Save</button></div> 1508 </form> 1509 </div> 1510 <div class="panel" x-show="advancedMode()"> 1511 <div class="settings-mode-row"> 1512 <div class="settings-mode-copy"> 1513 <div class="settings-mode-title">Stratum endpoint</div> 1514 <div class="muted" x-text="stratumEnabled ? 'ASIC miners can connect after the listener is active.' : 'Stratum listener is disabled.'"></div> 1515 </div> 1516 <label class="toggle-switch" :class="{ active: stratumEnabled }"> 1517 <input type="checkbox" :checked="stratumEnabled" @change="setStratumEnabled($event.target.checked)"> 1518 <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span> 1519 <span class="toggle-text" x-text="stratumEnabled ? 'On' : 'Off'"></span> 1520 </label> 1521 </div> 1522 <form class="settings-form" x-show="stratumEnabled" x-transition @submit.prevent="saveStratumSettings"> 1523 <label>Bind port<input x-model.number="stratumBindPort" @input="stratumBindPortDirty = true" type="number" min="1" max="65535" step="1" required></label> 1524 <div class="setup-actions"><button class="primary" type="submit">Save</button></div> 1525 </form> 1526 </div> 1527 <div class="panel"> 1528 <h3>Change Password</h3> 1529 <div class="setup-feedback" :class="settingsFeedback?.kind" x-show="settingsFeedback" x-transition x-text="settingsFeedback?.message"></div> 1530 <form class="settings-form" @submit.prevent="changePassword"> 1531 <input class="visually-hidden" type="text" name="username" value="iuna" autocomplete="username" tabindex="-1" aria-hidden="true"> 1532 <label>Current password<input x-model="settingsOldPassword" type="password" autocomplete="current-password" required></label> 1533 <label>New password<input x-model="settingsNewPassword" type="password" autocomplete="new-password" minlength="12" required></label> 1534 <label>Confirm new password<input x-model="settingsPasswordConfirm" type="password" autocomplete="new-password" minlength="12" required></label> 1535 <div class="setup-actions"><button class="primary" type="submit">Change password</button></div> 1536 </form> 1537 </div> 1538 <div class="panel"> 1539 <div class="settings-mode-row"> 1540 <div class="settings-mode-copy"> 1541 <div class="settings-mode-title">Development mode</div> 1542 <div class="muted" x-text="developmentMode() ? 'Detailed P2P data, metrics, and leaderboards are available.' : 'P2P stays focused on a simple peer list.'"></div> 1543 </div> 1544 <label class="toggle-switch" :class="{ active: keepTrackOfMetrics }"> 1545 <input type="checkbox" :checked="keepTrackOfMetrics" @change="setKeepTrackOfMetrics($event.target.checked)"> 1546 <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span> 1547 <span class="toggle-text" x-text="developmentMode() ? 'On' : 'Off'"></span> 1548 </label> 1549 </div> 1550 </div> 1551 <div class="panel danger-panel"> 1552 <h3>Danger Zone</h3> 1553 <div class="settings-mode-row"> 1554 <div class="settings-mode-copy"> 1555 <div class="settings-mode-title danger-title">Delete local chain</div> 1556 <div class="danger-copy">Remove the local blockchain database and request a fresh sync from connected peers. Wallet and settings stay on this device.</div> 1557 </div> 1558 <button class="danger" type="button" @click="openChainResetModal">Delete chain</button> 1559 </div> 1560 </div> 1561 </div> 1562 </section> 1563 </main> 1564 </div> 1565 <div class="setup-overlay" x-show="showingNetworkMigration()" x-transition.opacity role="dialog" aria-modal="true" aria-labelledby="network-migration-title"> 1566 <section class="setup-modal"> 1567 <div class="setup-modal-head"> 1568 <div class="setup-welcome">A new Iuna network is ready š</div> 1569 <h2 id="network-migration-title">Start from block 0</h2> 1570 <div class="setup-copy">This version connects to <strong x-text="migrationNetworkLabel()"></strong>. To join it, this node must remove its previous local blockchain and synchronize again.</div> 1571 </div> 1572 <div class="panel"> 1573 <h3>Your wallet is separate</h3> 1574 <p class="muted">Keeping your current wallet preserves its address and recovery phrase. Removing the local chain does not remove your wallet or settings.</p> 1575 <div class="setup-actions"> 1576 <button class="primary" type="button" :disabled="migrationBusy" @click="finishNetworkMigration(false)" x-text="migrationBusy ? 'Preparing...' : 'Keep wallet and resync'"></button> 1577 <button class="subtle" type="button" :disabled="migrationBusy" @click="finishNetworkMigration(true)">Use a different wallet</button> 1578 </div> 1579 <p class="danger-copy">Choose a different wallet only if you want a new address or need to import another recovery phrase. Without the old recovery phrase, its funds cannot be recovered on your new address.</p> 1580 </div> 1581 </section> 1582 </div> 1583 <div class="sync-overlay" x-show="syncingNode()" role="status" aria-live="polite" aria-label="Blockchain synchronization in progress"> 1584 <section class="sync-screen"> 1585 <div class="sync-mark" aria-hidden="true"><div class="sync-spinner"></div></div> 1586 <h1>Synchronizing blockchain</h1> 1587 <p class="sync-copy">Your node is catching up with the network. The interface will unlock automatically when synchronization is complete.</p> 1588 <div class="sync-progress" role="progressbar" aria-label="Blockchain sync progress" aria-valuemin="0" aria-valuemax="100" :aria-valuenow="Math.round(syncProgressPercent())"> 1589 <div class="sync-progress-fill" :style="`width: ${syncProgressPercent()}%`"></div> 1590 </div> 1591 <div class="sync-progress-label" x-text="syncProgressLabel()"></div> 1592 <button class="subtle" type="button" @click="openChainResetModal">Sync stuck? Reset local chain</button> 1593 <p class="muted">This recovery option removes only the local blockchain. Your wallet and settings stay on this device.</p> 1594 </section> 1595 </div> 1596 <div class="setup-overlay transaction-overlay" x-show="selectedPeerAddress" x-transition.opacity @click.self="closePeerModal()" role="dialog" aria-modal="true" aria-labelledby="peer-details-title"> 1597 <section class="tx-modal"> 1598 <div class="tx-modal-head"> 1599 <div class="tx-modal-title"> 1600 <span class="peer-status" :class="peerDetail() ? peerStatus(peerDetail()) : 'pending'" x-text="peerDetail() ? peerStatusLabel(peerDetail()) : 'Unavailable'"></span> 1601 <h2 id="peer-details-title">Peer details</h2> 1602 <code x-text="peerDetail()?.address || selectedPeerAddress || '-'"></code> 1603 </div> 1604 <button type="button" @click="closePeerModal">Close</button> 1605 </div> 1606 1607 <div class="tx-modal-empty" x-show="!peerDetail()">This peer is no longer present in the current peer list.</div> 1608 <template x-if="peerDetail()"> 1609 <div> 1610 <section class="peer-modal-section"> 1611 <h3>Connection</h3> 1612 <div class="peer-modal-grid"> 1613 <div class="peer-modal-field"><span class="tx-label">Direction</span><span class="peer-modal-value" x-text="peerDetail().direction"></span></div> 1614 <div class="peer-modal-field"><span class="tx-label">Country</span><span class="peer-modal-value" x-text="peerDetail().country_code || '-'"></span></div> 1615 <div class="peer-modal-field"><span class="tx-label">Last contact</span><span class="peer-modal-value" x-text="peerTimestampLabel(peerDetail().last_contact_ms)"></span></div> 1616 <div class="peer-modal-field"><span class="tx-label">Last success</span><span class="peer-modal-value" x-text="peerTimestampLabel(peerDetail().last_success_ms)"></span></div> 1617 <div class="peer-modal-field"><span class="tx-label">Messages sent</span><span class="peer-modal-value" x-text="peerDetail().messages_sent ?? 0"></span></div> 1618 <div class="peer-modal-field"><span class="tx-label">Messages received</span><span class="peer-modal-value" x-text="peerDetail().messages_received ?? 0"></span></div> 1619 </div> 1620 </section> 1621 1622 <section class="peer-modal-section"> 1623 <h3>Current chain view</h3> 1624 <div class="peer-modal-grid"> 1625 <div class="peer-modal-field"><span class="tx-label">Height</span><span class="peer-modal-value" x-text="peerDetail().last_known_height ?? '-'"></span></div> 1626 <div class="peer-modal-field wide"><span class="tx-label">Tip hash</span><code class="peer-modal-value" x-text="peerDetail().last_known_tip_hash || '-'"></code></div> 1627 </div> 1628 </section> 1629 1630 <section class="peer-modal-section"> 1631 <h3>Last protocol hello</h3> 1632 <div class="muted">These values are advertised by the remote peer during its handshake.</div> 1633 <div class="peer-modal-grid"> 1634 <div class="peer-modal-field"><span class="tx-label">Protocol version</span><span class="peer-modal-value" x-text="peerDetail().last_hello?.protocol_version ?? '-'"></span></div> 1635 <div class="peer-modal-field"><span class="tx-label">Network</span><code class="peer-modal-value" x-text="peerDetail().last_hello?.network_id || '-'"></code></div> 1636 <div class="peer-modal-field"><span class="tx-label">Advertised address</span><code class="peer-modal-value" x-text="peerDetail().last_hello?.listen_addr || '-'"></code></div> 1637 <div class="peer-modal-field"><span class="tx-label">Node ID</span><code class="peer-modal-value" x-text="peerDetail().last_hello?.node_id || '-'"></code></div> 1638 <div class="peer-modal-field"><span class="tx-label">Hello height</span><span class="peer-modal-value" x-text="peerDetail().last_hello?.height ?? '-'"></span></div> 1639 <div class="peer-modal-field"><span class="tx-label">Peer time</span><span class="peer-modal-value" x-text="peerTimestampLabel(peerDetail().last_hello?.time_ms)"></span></div> 1640 <div class="peer-modal-field wide"><span class="tx-label">Genesis hash</span><code class="peer-modal-value" x-text="peerDetail().last_hello?.genesis_hash || '-'"></code></div> 1641 <div class="peer-modal-field wide"><span class="tx-label">Hello tip hash</span><code class="peer-modal-value" x-text="peerDetail().last_hello?.tip_hash || '-'"></code></div> 1642 <div class="peer-modal-field wide"> 1643 <span class="tx-label">Capabilities</span> 1644 <div class="peer-capabilities" x-show="peerCapabilities(peerDetail()).length"> 1645 <template x-for="capability in peerCapabilities(peerDetail())" :key="capability"><code class="pill" x-text="capability"></code></template> 1646 </div> 1647 <span class="muted" x-show="peerCapabilities(peerDetail()).length === 0">None advertised</span> 1648 </div> 1649 </div> 1650 </section> 1651 1652 <section class="peer-modal-section"> 1653 <h3>Health and enforcement</h3> 1654 <div class="peer-modal-grid"> 1655 <div class="peer-modal-field"><span class="tx-label">Clock offset</span><span class="peer-modal-value" x-text="peerClockLabel(peerDetail())"></span></div> 1656 <div class="peer-modal-field"><span class="tx-label">Clock accepted</span><span class="peer-modal-value" x-text="peerDetail().last_clock_offset_accepted == null ? '-' : (peerDetail().last_clock_offset_accepted ? 'Yes' : 'No')"></span></div> 1657 <div class="peer-modal-field"><span class="tx-label">Clock observed</span><span class="peer-modal-value" x-text="peerTimestampLabel(peerDetail().last_clock_observed_ms)"></span></div> 1658 <div class="peer-modal-field"><span class="tx-label">Misbehavior score</span><span class="peer-modal-value" x-text="peerDetail().misbehavior_score ?? 0"></span></div> 1659 <div class="peer-modal-field"><span class="tx-label">Banned for</span><span class="peer-modal-value" x-text="peerBanLabel(peerDetail())"></span></div> 1660 <div class="peer-modal-field"><span class="tx-label">Banned until</span><span class="peer-modal-value" x-text="peerTimestampLabel(peerDetail().banned_until_ms)"></span></div> 1661 <div class="peer-modal-field"><span class="tx-label">Last error time</span><span class="peer-modal-value" x-text="peerTimestampLabel(peerDetail().last_error_ms)"></span></div> 1662 <div class="peer-modal-field wide"><span class="tx-label">Last error</span><span class="peer-modal-value" x-text="peerDetail().last_error || '-'"></span></div> 1663 <div class="peer-modal-field wide"><span class="tx-label">Ban reason</span><span class="peer-modal-value" x-text="peerDetail().ban_reason || '-'"></span></div> 1664 </div> 1665 </section> 1666 </div> 1667 </template> 1668 </section> 1669 </div> 1670 <div class="setup-overlay transaction-overlay" x-show="sendConfirmModalOpen" x-transition.opacity @click.self="closeSendConfirmModal()" role="dialog" aria-modal="true" aria-labelledby="send-confirm-title"> 1671 <section class="tx-modal address-book-modal"> 1672 <div class="tx-modal-head"> 1673 <div class="tx-modal-title"> 1674 <span class="pill transfer">Send</span> 1675 <h2 id="send-confirm-title">Confirm transfer</h2> 1676 </div> 1677 <button type="button" @click="closeSendConfirmModal" :disabled="sendConfirmBusy">Close</button> 1678 </div> 1679 <div class="info-copy"> 1680 <p>Check the full recipient address before sending.</p> 1681 <div class="info-fact"> 1682 <div class="label">Recipient</div> 1683 <code class="tx-value hash" x-text="pendingTransfer?.recipient || '-'"></code> 1684 </div> 1685 <div class="info-facts"> 1686 <div class="info-fact"> 1687 <div class="label">Amount</div> 1688 <div class="value">IUNA <span x-text="amountLabel(pendingTransfer?.amount || 0)"></span></div> 1689 </div> 1690 <div class="info-fact"> 1691 <div class="label">Fee / byte</div> 1692 <div class="value">IUNA <span x-text="amountLabel(pendingTransfer?.feePerByte || 0)"></span></div> 1693 </div> 1694 <div class="info-fact"> 1695 <div class="label">Transaction size</div> 1696 <div class="value"><span x-text="pendingTransfer?.bytes || 0"></span> bytes</div> 1697 </div> 1698 <div class="info-fact"> 1699 <div class="label">Network fee</div> 1700 <div class="value">IUNA <span x-text="amountLabel(pendingTransfer?.fee || 0)"></span></div> 1701 </div> 1702 </div> 1703 </div> 1704 <div class="danger-actions"> 1705 <button class="subtle" type="button" @click="closeSendConfirmModal" :disabled="sendConfirmBusy">Cancel</button> 1706 <button class="primary" type="button" @click="confirmTransfer" :disabled="sendConfirmBusy" x-text="sendConfirmBusy ? 'Sending...' : 'Confirm and send'"></button> 1707 </div> 1708 </section> 1709 </div> 1710 <div class="setup-overlay transaction-overlay" x-show="desktopUpdateModalOpen" x-transition.opacity @click.self="closeDesktopUpdateModal()" role="dialog" aria-modal="true" aria-labelledby="desktop-update-title"> 1711 <section class="tx-modal"> 1712 <div class="tx-modal-head"> 1713 <div class="tx-modal-title"> 1714 <span class="pill transfer">Update</span> 1715 <h2 id="desktop-update-title">Install <span x-text="latestReleaseLabel()"></span></h2> 1716 </div> 1717 <button type="button" @click="closeDesktopUpdateModal" :disabled="desktopUpdateBusy">Close</button> 1718 </div> 1719 <div class="info-copy"> 1720 <p>Iuna will download and verify the signed update, stop the local node, install it, and restart the desktop app.</p> 1721 <p>Your wallet, settings, and local chain data stay on this device.</p> 1722 </div> 1723 <div class="danger-actions"> 1724 <button class="subtle" type="button" @click="closeDesktopUpdateModal" :disabled="desktopUpdateBusy">Later</button> 1725 <button class="primary" type="button" @click="installDesktopUpdate" :disabled="desktopUpdateBusy" x-text="desktopUpdateBusy ? 'Installing...' : 'Install and restart'"></button> 1726 </div> 1727 </section> 1728 </div> 1729 <div class="setup-overlay transaction-overlay" x-show="chainResetModalOpen" x-transition.opacity @click.self="closeChainResetModal()" role="dialog" aria-modal="true" aria-labelledby="chain-reset-title"> 1730 <section class="tx-modal"> 1731 <div class="tx-modal-head"> 1732 <div class="tx-modal-title"> 1733 <span class="pill error">Danger</span> 1734 <h2 id="chain-reset-title">Delete local chain</h2> 1735 </div> 1736 <button type="button" @click="closeChainResetModal" :disabled="chainResetBusy">Close</button> 1737 </div> 1738 <div class="info-copy"> 1739 <p>This deletes the local chain database and clears local chain views. Your wallet and settings stay intact.</p> 1740 <p>Type <strong>RESET</strong> to confirm.</p> 1741 </div> 1742 <form class="settings-form" @submit.prevent="resetLocalChain"> 1743 <label>Confirmation<input x-model="chainResetConfirm" autocomplete="off" spellcheck="false" placeholder="RESET"></label> 1744 <div class="danger-actions"> 1745 <button class="subtle" type="button" @click="closeChainResetModal" :disabled="chainResetBusy">Cancel</button> 1746 <button class="danger" type="submit" :disabled="chainResetConfirm.trim() !== 'RESET' || chainResetBusy" x-text="chainResetBusy ? 'Deleting...' : 'Delete and resync'"></button> 1747 </div> 1748 </form> 1749 </section> 1750 </div> 1751 <div class="setup-overlay" x-show="showingAuth()" x-transition.opacity role="dialog" aria-modal="true" aria-labelledby="auth-title"> 1752 <section class="setup-modal auth-form"> 1753 <div class="setup-modal-head"> 1754 <div class="setup-welcome">iuna Access</div> 1755 <h2 id="auth-title" x-text="auth.configured ? 'Unlock iuna' : 'Set Password'"></h2> 1756 <div class="setup-copy" x-show="!auth.configured">Choose a local password before wallet setup continues.</div> 1757 <div class="setup-copy" x-show="auth.configured">Enter the local password to unlock this node.</div> 1758 </div> 1759 <div class="setup-feedback" :class="authFeedback?.kind" x-show="authFeedback" x-transition x-text="authFeedback?.message"></div> 1760 <form x-show="!auth.configured" @submit.prevent="setupPassword"> 1761 <input class="visually-hidden" type="text" name="username" value="iuna" autocomplete="username" tabindex="-1" aria-hidden="true"> 1762 <label>Password<input x-model="authPassword" type="password" autocomplete="new-password" minlength="12" required></label> 1763 <label>Confirm password<input x-model="authPasswordConfirm" type="password" autocomplete="new-password" minlength="12" required></label> 1764 <div class="setup-actions"><button class="primary" type="submit">Set password</button></div> 1765 </form> 1766 <form x-show="auth.configured && !auth.authenticated" @submit.prevent="login"> 1767 <input class="visually-hidden" type="text" name="username" value="iuna" autocomplete="username" tabindex="-1" aria-hidden="true"> 1768 <label>Password<input x-model="loginPassword" type="password" autocomplete="current-password" required></label> 1769 <div class="setup-actions"><button class="primary" type="submit">Unlock</button></div> 1770 </form> 1771 </section> 1772 </div> 1773 <div class="setup-overlay transaction-overlay" x-show="optimizeOpen" x-transition.opacity @click.self="closeOptimizeWallet()" @keydown.escape.stop="closeOptimizeWallet()" role="dialog" aria-modal="true" aria-labelledby="optimize-title"> 1774 <section class="tx-modal"> 1775 <div class="tx-modal-head"> 1776 <div class="tx-modal-title"><h2 id="optimize-title">Optimize wallet</h2></div> 1777 <button type="button" @click="closeOptimizeWallet" :disabled="optimizeRunning">Close</button> 1778 </div> 1779 <p>Your balance is made up of many small parts. Combining them can make future payments simpler and cheaper.</p> 1780 <p>Your money stays in your wallet. You only pay the network fee shown below. Parts that would be relatively expensive to combine are skipped.</p> 1781 <p class="muted">Only available parts are combined. The largest part is kept separate so you can continue making payments.</p> 1782 <form class="optimize-form" @submit.prevent="previewOptimization"> 1783 <label>Network fee / byte (IUNA)<input type="number" min="0.000001" step="0.000001" x-model="optimizeFee" :disabled="optimizeBusy || optimizeRunning || !!optimizePlan" required></label> 1784 <label class="optimize-choice"><input type="checkbox" x-model="optimizeMergeRoots" :disabled="optimizeBusy || optimizeRunning || !!optimizePlan"> Also combine different mining groups</label> 1785 <p class="panel-description">Mining groups are kept separate by default. Combining different groups changes their lineage and can affect burn-committee selection and maturity.</p> 1786 <button type="submit" :disabled="optimizeBusy || optimizeRunning || !!optimizePlan || status.wallet_locked" x-text="optimizeBusy ? 'Calculatingā¦' : 'Preview costs'"></button> 1787 <span x-show="status.wallet_locked">Unlock your wallet to preview.</span> 1788 </form> 1789 <template x-if="optimizePlan"> 1790 <div> 1791 <div class="tx-modal-summary" style="margin-top:16px"> 1792 <div class="tx-field"><span class="tx-label">Balance parts (expected)</span><span class="tx-value" x-text="`${optimizePlan.before} ā ${optimizePlan.after}`"></span></div> 1793 <div class="tx-field"><span class="tx-label">Maximum total fee</span><span class="tx-value" x-text="`${amountLabel(optimizePlan.fee)} IUNA`"></span></div> 1794 <div class="tx-field"><span class="tx-label">Transactions</span><span class="tx-value" x-text="optimizePlan.batches.length"></span></div> 1795 </div> 1796 <p class="muted">After confirmation, all batches are submitted automatically. The modal closes when they are queued; confirmation can continue in the background. Fees are fixed by this preview and never raised automatically.</p> 1797 <p x-show="optimizePlan.batches.length === 0">No suitable batches at this fee and mining-group setting. You can leave your wallet as it is. Small or reserved parts and separate mining groups may remain.</p> 1798 <p x-show="optimizePlan.batches.length === 32">This preview covers up to 32 batches. You can review another optimization afterwards.</p> 1799 <button class="primary" type="button" x-show="optimizePlan.batches.length > 0 && !optimizeRunning" @click="runOptimization">Confirm and optimize</button> 1800 <button type="button" x-show="!optimizeRunning" @click="optimizePlan = null; optimizeError = ''; optimizeMessage = ''">Change settings</button> 1801 </div> 1802 </template> 1803 <p role="status" aria-live="polite" x-text="optimizeMessage"></p> 1804 <p class="fee-warning" role="alert" x-show="optimizeError" x-text="optimizeError"></p> 1805 </section> 1806 </div> 1807 <div class="setup-overlay transaction-overlay" x-show="showWalletUtxos" x-transition.opacity @click.self="closeWalletUtxosModal()" role="dialog" aria-modal="true" aria-labelledby="wallet-utxos-title"> 1808 <section class="tx-modal"> 1809 <div class="tx-modal-head"> 1810 <div class="tx-modal-title"> 1811 <h2 id="wallet-utxos-title">Wallet UTXOs</h2> 1812 <button type="button" @click="openOptimizeWallet">Optimize wallet</button> 1813 <div class="tx-field"><span class="tx-label">Total</span><span class="tx-value money">IUNA <span x-text="amountLabel(status.wallet_balance)"></span></span></div> 1814 </div> 1815 <button type="button" @click="closeWalletUtxosModal">Close</button> 1816 </div> 1817 <div class="utxo-list"> 1818 <template x-for="utxo in walletUtxos" :key="`${utxo.outpoint.txid}:${utxo.outpoint.index}`"> 1819 <div class="wallet-utxo-row"> 1820 <div class="utxo-node-label"><span>UTXO</span><span class="utxo-node-amount">IUNA <span x-text="amountLabel(utxo.amount)"></span></span></div> 1821 <div class="tx-field"><span class="tx-label">Outpoint</span><code class="tx-value hash" x-text="txInputOutpoint({ outpoint: utxo.outpoint })"></code></div> 1822 <div class="tx-field"><span class="tx-label">Address</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="addressLabel(utxo.address)" @click="openAddressContact(utxo.address)" @keydown.enter.prevent="openAddressContact(utxo.address)" @keydown.space.prevent="openAddressContact(utxo.address)" title="Add or edit contact"></code></div> 1823 </div> 1824 </template> 1825 <div class="dataset-loader" x-show="walletUtxoPage.loading" aria-hidden="true"> 1826 <div class="wallet-utxo-row skeleton-card"><div class="skeleton-line medium"></div><div class="skeleton-line long"></div></div> 1827 <div class="wallet-utxo-row skeleton-card"><div class="skeleton-line short"></div><div class="skeleton-line long"></div></div> 1828 </div> 1829 <div class="page-sentinel" x-show="walletUtxoPage.hasMore" x-init="$nextTick(() => observePageSentinel('walletUtxo', $el))"></div> 1830 <div class="tx-modal-empty" x-show="walletUtxos.length === 0 && !walletUtxoPage.loading">No wallet UTXOs</div> 1831 </div> 1832 </section> 1833 </div> 1834 <div class="setup-overlay transaction-overlay" x-show="showWalletAddresses" x-transition.opacity @click.self="closeWalletAddressesModal()" @keydown.escape.stop="closeWalletAddressesModal()" role="dialog" aria-modal="true" aria-labelledby="wallet-addresses-title"> 1835 <section class="tx-modal"> 1836 <div class="tx-modal-head"> 1837 <div class="tx-modal-title"> 1838 <h2 id="wallet-addresses-title">Funded wallet addresses</h2> 1839 <div class="muted">Previous receive and reward addresses remain monitored automatically.</div> 1840 </div> 1841 <button type="button" @click="closeWalletAddressesModal">Close</button> 1842 </div> 1843 <div class="wallet-address-list"> 1844 <template x-for="entry in fundedWalletAddresses()" :key="entry.address"> 1845 <div class="wallet-address-row"> 1846 <div class="wallet-address-row-head"> 1847 <span class="wallet-address-state" :class="{ pending: walletAddressHasPendingSpend(entry) }" x-text="walletAddressState(entry)"></span> 1848 <span class="utxo-node-amount">IUNA <span x-text="amountLabel(entry.balance)"></span></span> 1849 </div> 1850 <code class="tx-value hash" x-text="entry.address"></code> 1851 <div class="muted" x-text="walletAddressUtxoLabel(entry)"></div> 1852 </div> 1853 </template> 1854 <div class="tx-modal-empty" x-show="fundedWalletAddresses().length === 0">No funded wallet addresses</div> 1855 </div> 1856 </section> 1857 </div> 1858 <div class="setup-overlay transaction-overlay" x-show="showPowDifficultyInfo" x-transition.opacity @click.self="closePowDifficultyInfo()" role="dialog" aria-modal="true" aria-labelledby="pow-difficulty-title"> 1859 <section class="tx-modal"> 1860 <div class="tx-modal-head"> 1861 <div class="tx-modal-title"> 1862 <h2 id="pow-difficulty-title">PoW Difficulty</h2> 1863 </div> 1864 <button type="button" @click="closePowDifficultyInfo">Close</button> 1865 </div> 1866 <div class="info-copy"> 1867 <p>Difficulty is adjusted to target about one mine action per block.</p> 1868 <div class="info-facts"> 1869 <div class="info-fact"><div class="label">Window</div><div class="value">10 blocks</div></div> 1870 <div class="info-fact"><div class="label">Target</div><div class="value">10 mine actions</div></div> 1871 <div class="info-fact"><div class="label">Max step</div><div class="value">2 bits</div></div> 1872 </div> 1873 <p>If a window includes more mine actions than the target, difficulty rises. If it includes fewer, difficulty falls. The initial difficulty is 12 bits.</p> 1874 </div> 1875 </section> 1876 </div> 1877 <div class="setup-overlay transaction-overlay" x-show="selectedByteBlock" x-transition.opacity @click.self="closeBlockBytesModal()" role="dialog" aria-modal="true" aria-labelledby="block-bytes-title"> 1878 <section class="tx-modal"> 1879 <div class="tx-modal-head"> 1880 <div class="tx-modal-title"> 1881 <h2 id="block-bytes-title" x-text="selectedByteBlock ? `Block ${selectedByteBlock.height} Storage` : 'Block Storage'"></h2> 1882 <div class="tx-field"><span class="tx-label">Total</span><span class="tx-value number"><span x-text="blockTotalBytes(selectedByteBlock)"></span>B</span></div> 1883 </div> 1884 <button type="button" @click="closeBlockBytesModal">Close</button> 1885 </div> 1886 <div class="rank-list"> 1887 <template x-for="row in blockByteBreakdown(selectedByteBlock)" :key="row[0]"> 1888 <div class="rank-row"> 1889 <div class="rank-number" x-text="`${row[1]}B`"></div> 1890 <div class="rank-details"> 1891 <div class="tx-field"> 1892 <span class="tx-label">Category</span> 1893 <span class="pill" x-show="row[2]" :class="row[2]" x-text="row[0]"></span> 1894 <span class="tx-value text" x-show="!row[2]" x-text="row[0]"></span> 1895 </div> 1896 </div> 1897 </div> 1898 </template> 1899 </div> 1900 </section> 1901 </div> 1902 <div class="setup-overlay transaction-overlay" x-show="selectedBurnBundleBlock" x-transition.opacity @click.self="closeBurnBundleModal()" role="dialog" aria-modal="true" aria-labelledby="burn-bundles-title"> 1903 <section class="tx-modal"> 1904 <div class="tx-modal-head"> 1905 <div class="tx-modal-title"> 1906 <h2 id="burn-bundles-title" x-text="selectedBurnBundleBlock ? `Block ${selectedBurnBundleBlock.height} Burn Bundles` : 'Burn Bundles'"></h2> 1907 <div class="tx-field"><span class="tx-label">Attestations</span><span class="tx-value number" x-text="blockBurnBundleRatio(selectedBurnBundleBlock)"></span></div> 1908 </div> 1909 <button type="button" @click="closeBurnBundleModal">Close</button> 1910 </div> 1911 <div class="bundle-summary" x-show="blockBurnBundleSlots(selectedBurnBundleBlock).length > 0"> 1912 <strong><span x-text="blockBurnBundleRatio(selectedBurnBundleBlock)"></span> attestations committed</strong> 1913 <span class="muted">The first number is the attestations used by this block. The second is the committed committee represented in the blockānot every candidate wallet considered by the network.</span> 1914 </div> 1915 <div class="bundle-flow" x-show="blockBurnBundleSlots(selectedBurnBundleBlock).length > 0"> 1916 <template x-for="slot in blockBurnBundleSlots(selectedBurnBundleBlock)" :key="`${selectedBurnBundleBlock.hash}-${slot.slot}`"> 1917 <div class="bundle-slot"> 1918 <div class="bundle-slot-head"> 1919 <span class="bundle-slot-number" x-text="slot.slot"></span> 1920 <span class="bundle-slot-status">Included</span> 1921 </div> 1922 <div class="tx-field"><span class="tx-label" x-text="slot.role"></span><code class="wallet-address-link" role="button" tabindex="0" x-text="addressLabel(slot.member)" @click="openAddressContact(slot.member)" @keydown.enter.prevent="openAddressContact(slot.member)" @keydown.space.prevent="openAddressContact(slot.member)" title="Add or edit contact"></code></div> 1923 <div class="tx-field"><span class="tx-label">Contribution</span><span class="tx-value text" x-text="burnBundleSlotDetail(slot)"></span></div> 1924 <div class="tx-field" x-show="slot.hash"><span class="tx-label">Bundle</span><code x-text="short(slot.hash)"></code></div> 1925 </div> 1926 </template> 1927 </div> 1928 <div class="tx-modal-empty" x-show="blockBurnBundleSlots(selectedBurnBundleBlock).length === 0">Recovery blocks do not require burn-bundle attestations.</div> 1929 <div class="bundle-how" x-show="blockBurnBundleSlots(selectedBurnBundleBlock).length > 0"> 1930 <h3>How are they chosen?</h3> 1931 <div class="bundle-how-step"><span>1</span><span><strong>Finalizer:</strong> the burn-ticket ranking chooses the block finalizer, which implicitly fills slot 0.</span></div> 1932 <div class="bundle-how-step"><span>2</span><span><strong>Root groups:</strong> mature UTXO lineage weight selects independent groups for the additional committee slots. One root can win at most one slot.</span></div> 1933 <div class="bundle-how-step"><span>3</span><span><strong>Wallet:</strong> a wallet with a valid ticket represents each winning root group. Its burn amount does not increase that root's committee weight.</span></div> 1934 <div class="bundle-how-step"><span>4</span><span><strong>Burn list:</strong> each member bundles valid pending burns by highest absolute fee, using the signature as the deterministic tie-breaker.</span></div> 1935 <p class="muted">This block records the included members and attestations. Historical lineage weights and candidates that were not included are not stored in the block.</p> 1936 </div> 1937 </section> 1938 </div> 1939 <div class="setup-overlay transaction-overlay" x-show="selectedBurnLeaderBlock" x-transition.opacity @click.self="closeBurnLeaderRanksModal()" role="dialog" aria-modal="true" aria-labelledby="burn-ranks-title"> 1940 <section class="tx-modal"> 1941 <div class="tx-modal-head"> 1942 <div class="tx-modal-title"> 1943 <h2 id="burn-ranks-title" x-text="burnLeaderRanksTitle(selectedBurnLeaderBlock)"></h2> 1944 <div class="tx-field"><span class="tx-label">Finalizer</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="selectedBurnLeaderBlock ? addressLabel(selectedBurnLeaderBlock.miner) : '-'" @click="openAddressContact(selectedBurnLeaderBlock?.miner)" @keydown.enter.prevent="openAddressContact(selectedBurnLeaderBlock?.miner)" @keydown.space.prevent="openAddressContact(selectedBurnLeaderBlock?.miner)" title="Add or edit contact"></code></div> 1945 </div> 1946 <button type="button" @click="closeBurnLeaderRanksModal">Close</button> 1947 </div> 1948 <div class="rank-list"> 1949 <template x-for="rank in burnLeaderRanks(selectedBurnLeaderBlock)" :key="`${selectedBurnLeaderBlock.hash}-${rank.rank}-${rank.ticket_id ?? rank.ticketId}`"> 1950 <div class="rank-row"> 1951 <div class="rank-number" x-text="burnLeaderRankLabel(rank)"></div> 1952 <div class="rank-details"> 1953 <div class="tx-field"><span class="tx-label">Owner</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="addressLabel(rank.owner)" @click="openAddressContact(rank.owner)" @keydown.enter.prevent="openAddressContact(rank.owner)" @keydown.space.prevent="openAddressContact(rank.owner)" title="Add or edit contact"></code></div> 1954 <div class="tx-field"><span class="tx-label">Burn</span><span class="tx-value money">IUNA <span x-text="amountLabel(rank.amount)"></span></span></div> 1955 <div class="tx-field"><span class="tx-label">Ticket</span><code class="tx-value hash" x-text="short(rank.ticket_id ?? rank.ticketId)"></code></div> 1956 <div class="tx-field"><span class="tx-label">Eligible</span><span class="tx-value number" x-text="burnLeaderEligibilityLabel(rank)"></span></div> 1957 </div> 1958 </div> 1959 </template> 1960 <div class="tx-modal-empty" x-show="burnLeaderRanks(selectedBurnLeaderBlock).length === 0">No burn leader ranks</div> 1961 </div> 1962 </section> 1963 </div> 1964 <div class="setup-overlay transaction-overlay" x-show="selectedTransaction" x-transition.opacity @click.self="closeTransactionModal()" role="dialog" aria-modal="true" aria-labelledby="tx-modal-title"> 1965 <section class="tx-modal"> 1966 <div class="tx-modal-head"> 1967 <div class="tx-modal-title"> 1968 <span class="pill" :class="txPillClass(selectedTransaction?.tx)" x-text="txPillLabel(selectedTransaction?.tx)"></span> 1969 <h2 id="tx-modal-title">Transaction</h2> 1970 <code class="tx-value hash" x-text="selectedTransaction?.tx?.signature || '-'"></code> 1971 </div> 1972 <button type="button" @click="closeTransactionModal">Close</button> 1973 </div> 1974 <div class="tx-modal-summary"> 1975 <div class="tx-field"><span class="tx-label">Source</span><span class="tx-value text" x-text="selectedTransactionLabel()"></span></div> 1976 <div class="tx-field"><span class="tx-label">Amount</span><span class="tx-value money">IUNA <span x-text="amountLabel(txAmount(selectedTransaction?.tx || {}))"></span></span></div> 1977 <div class="tx-field" x-show="!isRewardTx(selectedTransaction?.tx)"><span class="tx-label">Fee</span><span class="tx-value money" x-text="txFeeLabel(selectedTransaction?.tx)"></span></div> 1978 <div class="tx-field" x-show="!isRewardTx(selectedTransaction?.tx)"><span class="tx-label">From</span><code class="tx-value hash" :class="{ 'wallet-address-link': hasWalletAddress(txFrom(selectedTransaction?.tx || {})) }" role="button" :tabindex="hasWalletAddress(txFrom(selectedTransaction?.tx || {})) ? 0 : -1" x-text="addressLabel(txFrom(selectedTransaction?.tx || {}))" @click="openAddressContact(txFrom(selectedTransaction?.tx || {}))" @keydown.enter.prevent="openAddressContact(txFrom(selectedTransaction?.tx || {}))" @keydown.space.prevent="openAddressContact(txFrom(selectedTransaction?.tx || {}))" :title="hasWalletAddress(txFrom(selectedTransaction?.tx || {})) ? 'Add or edit contact' : null"></code></div> 1979 <div class="tx-field" x-show="!isRewardTx(selectedTransaction?.tx) && txTo(selectedTransaction?.tx || {})"><span class="tx-label">To</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="addressLabel(txTo(selectedTransaction?.tx || {}))" @click="openAddressContact(txTo(selectedTransaction?.tx || {}))" @keydown.enter.prevent="openAddressContact(txTo(selectedTransaction?.tx || {}))" @keydown.space.prevent="openAddressContact(txTo(selectedTransaction?.tx || {}))" title="Add or edit contact"></code></div> 1980 <div class="tx-field" x-show="isMineTx(selectedTransaction?.tx)"><span class="tx-label">Difficulty</span><span class="tx-value number" x-text="txDifficultyBits(selectedTransaction?.tx) ?? '-'"></span></div> 1981 <div class="tx-field" x-show="isMineTx(selectedTransaction?.tx)"><span class="tx-label">Proof Bits</span><span class="tx-value number" x-text="txProofBits(selectedTransaction?.tx) ?? '-'"></span></div> 1982 <div class="tx-field" x-show="isMineTx(selectedTransaction?.tx)"><span class="tx-label">Proof Hash</span><code class="tx-value hash" x-text="txProofHash(selectedTransaction?.tx) || '-'"></code></div> 1983 </div> 1984 <div class="utxo-flow"> 1985 <div class="utxo-column"> 1986 <h3>Inputs</h3> 1987 <template x-for="(input, index) in txInputs(selectedTransaction?.tx || {})" :key="txInputKey(input, index)"> 1988 <div class="utxo-node"> 1989 <div class="utxo-node-label"><span x-text="input.rewardFee ? `Paid fee ${index + 1}` : `Input ${index + 1}`"></span><span x-text="input.rewardFee ? input.transactionKind : 'spent'"></span></div> 1990 <div class="utxo-node-ref" x-text="txInputOutpoint(input)"></div> 1991 <div class="tx-field"><span class="tx-label">Value</span><span class="tx-value money" x-text="txInputAmountLabel(input)"></span></div> 1992 <div class="tx-field" x-show="input.owner"><span class="tx-label" x-text="input.rewardFee ? 'Paid by' : 'Owner'"></span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="addressLabel(input.owner)" @click="openAddressContact(input.owner)" @keydown.enter.prevent="openAddressContact(input.owner)" @keydown.space.prevent="openAddressContact(input.owner)" title="Add or edit contact"></code></div> 1993 <div class="tx-field"><span class="tx-label" x-text="input.rewardFee ? 'Transaction' : 'Sig'"></span><code class="tx-value hash" x-text="short(input.signature)"></code></div> 1994 </div> 1995 </template> 1996 <div class="tx-modal-empty" x-show="txInputs(selectedTransaction?.tx || {}).length === 0">No inputs</div> 1997 </div> 1998 <div class="utxo-arrow" aria-hidden="true">→</div> 1999 <div class="utxo-column"> 2000 <h3>Outputs</h3> 2001 <template x-for="(output, index) in txVisualOutputs(selectedTransaction?.tx || {})" :key="txOutputKey(output, index)"> 2002 <div class="utxo-node" :class="{ burned: output.kind === 'burned', fee: output.kind === 'fee', reward: output.kind === 'reward' }"> 2003 <div class="utxo-node-label"><span x-text="output.label"></span><span x-text="output.kind"></span></div> 2004 <div class="utxo-node-amount">IUNA <span x-text="amountLabel(output.amount)"></span></div> 2005 <template x-if="output.address"> 2006 <div class="tx-field"><span class="tx-label">To</span><code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="addressLabel(output.address)" @click="openAddressContact(output.address)" @keydown.enter.prevent="openAddressContact(output.address)" @keydown.space.prevent="openAddressContact(output.address)" title="Add or edit contact"></code></div> 2007 </template> 2008 <template x-if="output.detail"> 2009 <div class="tx-field"><span class="tx-label" x-text="output.detailLabel"></span><code class="tx-value hash" x-text="output.detail"></code></div> 2010 </template> 2011 </div> 2012 </template> 2013 <div class="tx-modal-empty" x-show="txVisualOutputs(selectedTransaction?.tx || {}).length === 0">No outputs</div> 2014 </div> 2015 </div> 2016 </section> 2017 </div> 2018 <div class="setup-overlay transaction-overlay" x-show="addressBookPickerOpen" x-transition.opacity @click.self="closeAddressBookPicker()" role="dialog" aria-modal="true" aria-labelledby="address-book-picker-title"> 2019 <section class="tx-modal address-book-modal"> 2020 <div class="tx-modal-head"> 2021 <div class="tx-modal-title"> 2022 <span class="pill" x-text="addressBookStandalone ? 'Contact' : 'Send'"></span> 2023 <h2 id="address-book-picker-title" x-text="addressBookStandalone ? (addressBookEditingAddress ? 'Edit Contact' : 'Add Contact') : 'Choose Contact'"></h2> 2024 </div> 2025 <div class="address-book-actions"> 2026 <button class="icon-button" type="button" x-show="!addressBookModalOpen" @click="openAddressBookModal(null, false)" title="Add contact" aria-label="Add contact"> 2027 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14"></path><path d="M5 12h14"></path></svg> 2028 </button> 2029 <button class="icon-button" type="button" @click="closeAddressBookPicker()" title="Close" aria-label="Close"> 2030 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg> 2031 </button> 2032 </div> 2033 </div> 2034 <div class="address-book-picker-list" x-show="!addressBookModalOpen"> 2035 <template x-for="entry in addressBookEntries()" :key="entry.address"> 2036 <button class="address-book-picker-row" type="button" @click="selectTransferContact(entry.address)" :title="`Send to ${entry.name}`"> 2037 <span class="address-book-name" x-text="entry.name"></span> 2038 <code class="tx-value hash" x-text="short(entry.address)"></code> 2039 </button> 2040 </template> 2041 <div class="tx-modal-empty" x-show="addressBookEntries().length === 0">No saved addresses</div> 2042 </div> 2043 <form class="address-book-form" x-show="addressBookModalOpen" @submit.prevent="saveAddressBookEntry"> 2044 <label>Name<input x-model="addressBookDraftName" autocomplete="off" required></label> 2045 <label>Address<input x-model="addressBookDraftAddress" autocomplete="off" required :class="{ invalid: addressBookDraftAddress && !validAddressBookAddress(addressBookDraftAddress) }"></label> 2046 <div class="setup-feedback error" x-show="addressBookDraftAddress && !validAddressBookAddress(addressBookDraftAddress)">Address must be a Bech32m address for this network</div> 2047 <div class="address-book-modal-actions"> 2048 <button class="icon-button modal-delete-button" type="button" x-show="addressBookEditingAddress" @click="removeAddressBookEntry({ address: addressBookEditingAddress, name: addressBookDraftName || addressBookEditingAddress })" title="Delete contact" aria-label="Delete contact"> 2049 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M6 7l1 14h10l1-14"></path><path d="M9 7V4h6v3"></path></svg> 2050 </button> 2051 <button class="icon-button" type="button" @click="closeAddressBookModal()" :title="addressBookStandalone ? 'Cancel' : 'Back to contacts'" :aria-label="addressBookStandalone ? 'Cancel' : 'Back to contacts'"> 2052 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6"></path></svg> 2053 </button> 2054 <button class="primary icon-button" type="submit" title="Save contact" aria-label="Save contact"> 2055 <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z"></path><path d="M7 3v6h8"></path><path d="M7 21v-8h10v8"></path></svg> 2056 </button> 2057 </div> 2058 </form> 2059 </section> 2060 </div> 2061 <div class="setup-overlay" x-show="showingSetup()" x-transition.opacity role="dialog" aria-modal="true" aria-labelledby="setup-title"> 2062 <section class="setup-modal"> 2063 <div class="setup-modal-head"> 2064 <div class="setup-welcome">Welcome to iuna</div> 2065 <h2 id="setup-title">Initial Setup</h2> 2066 <div class="setup-copy">Connect this node to the network, then set up the local wallet.</div> 2067 </div> 2068 <div class="setup-feedback" :class="setupFeedback?.kind" x-show="setupFeedback" x-transition x-text="setupFeedback?.message"></div> 2069 <div class="setup-grid"> 2070 <div class="setup-section setup-node-mode"> 2071 <div class="panel-head"> 2072 <h3>Mode</h3> 2073 <span class="pill">Change later in Settings</span> 2074 </div> 2075 <div class="segmented setup-mode-picker" role="tablist" aria-label="Initial node mode"> 2076 <button type="button" :class="{ active: setupNodeMode === 'wallet' }" @click="selectSetupNodeMode('wallet')">Wallet</button> 2077 <button type="button" :class="{ active: setupNodeMode === 'non-listening' }" @click="selectSetupNodeMode('non-listening')">Non-listening node</button> 2078 <button type="button" :class="{ active: setupNodeMode === 'listening' }" @click="selectSetupNodeMode('listening')">Listening node</button> 2079 </div> 2080 <div class="setup-network-copy" x-text="setupNodeModeCopy()"></div> 2081 </div> 2082 <div class="setup-section setup-network"> 2083 <div class="panel-head"> 2084 <h3>Network</h3> 2085 <a class="setup-network-link" href="https://getiuna.org/git/iuna/file/KNOWN_NODES.txt.html" target="_blank" rel="noreferrer">Known nodes</a> 2086 </div> 2087 <div class="setup-network-row"> 2088 <label><span x-text="setupRequiresPeer() ? 'Bootstrap peer (required)' : 'Bootstrap peer'"></span><input x-model="setupPeerAddress" placeholder="iuna.jhx.app:9444"></label> 2089 <label x-show="setupNodeMode === 'listening'" x-transition>Bind port<input x-model.number="p2pBindPort" @input="p2pBindPortDirty = true" type="number" min="1" max="65535" step="1" required></label> 2090 </div> 2091 <div class="setup-network-copy" x-text="setupRequiresPeer() ? 'A bootstrap peer is required before this node can join the network. Known nodes help discovery; they do not control your wallet or decide valid blocks.' : 'You can add a bootstrap peer now or later from the P2P screen. Known nodes help discovery; they do not control your wallet or decide valid blocks.'"></div> 2092 </div> 2093 <div class="setup-section setup-wallet-section seed-panel"> 2094 <div class="panel-head"> 2095 <h3>Wallet</h3> 2096 </div> 2097 <div class="segmented" role="tablist" aria-label="Wallet setup mode"> 2098 <button type="button" :class="{ active: setupWalletMode === 'create' }" @click="selectSetupWalletMode('create')">Create</button> 2099 <button type="button" :class="{ active: setupWalletMode === 'import' }" @click="selectSetupWalletMode('import')">Import</button> 2100 </div> 2101 <div x-show="setupWalletMode === 'create'" class="seed-panel"> 2102 <div class="setup-field"> 2103 <div class="setup-field-label">Address</div> 2104 <div class="address-box setup-address-box"> 2105 <code class="wallet-address-link" role="button" tabindex="0" x-text="setupAddress()" @click="openAddressContact(setupAddress())" @keydown.enter.prevent="openAddressContact(setupAddress())" @keydown.space.prevent="openAddressContact(setupAddress())" title="Add or edit contact"></code> 2106 <button type="button" @click="copyAddress">Copy</button> 2107 </div> 2108 </div> 2109 <template x-if="setupSeedWords().length > 0 && setupSeedStep === 'write'"> 2110 <div class="seed-panel"> 2111 <div class="setup-field"> 2112 <div class="setup-field-label">Recovery phrase</div> 2113 <div class="seed-grid"> 2114 <template x-for="(word, index) in setupSeedWords()" :key="index"> 2115 <div class="seed-word"> 2116 <span class="index" x-text="index + 1"></span> 2117 <span class="word" x-text="word"></span> 2118 </div> 2119 </template> 2120 </div> 2121 </div> 2122 <div class="setup-actions"> 2123 <button type="button" class="subtle" @click="generateSetupSeed">Regenerate</button> 2124 <button type="button" class="subtle" x-show="setupWallet.dev_verify_bypass" @click="skipSeedVerificationForDev">Skip verification</button> 2125 <button type="button" class="primary" @click="beginSeedVerification">I wrote it down</button> 2126 </div> 2127 </div> 2128 </template> 2129 <template x-if="setupSeedWords().length === 0"> 2130 <div class="seed-panel"> 2131 <div class="muted">This wallet does not have a recovery phrase yet.</div> 2132 <button type="button" class="primary" @click="generateSetupSeed">Generate recovery phrase</button> 2133 </div> 2134 </template> 2135 <template x-if="setupSeedStep === 'verify'"> 2136 <div class="seed-panel"> 2137 <div class="verify-grid"> 2138 <template x-for="challenge in verifyChallenges" :key="challenge.index"> 2139 <label> 2140 <span>Word <span x-text="challenge.position"></span></span> 2141 <input x-model="verifyAnswers[challenge.index]" autocomplete="off"> 2142 </label> 2143 </template> 2144 </div> 2145 <div class="setup-actions"> 2146 <button type="button" class="subtle" @click="setupSeedStep = 'write'">Back</button> 2147 <button type="button" class="primary" @click="verifyGeneratedSeed">Verify</button> 2148 </div> 2149 </div> 2150 </template> 2151 <template x-if="setupSeedStep === 'verified' && walletVerified"> 2152 <div class="setup-status">Recovery phrase verified</div> 2153 </template> 2154 </div> 2155 <div x-show="setupWalletMode === 'import'" class="seed-panel"> 2156 <form @submit.prevent="importSetupSeed"> 2157 <label>Recovery phrase<textarea x-model="importSeedPhrase" autocomplete="off" spellcheck="false" placeholder="24 words, separated by spaces or new lines"></textarea></label> 2158 <button class="primary" type="submit">Import</button> 2159 </form> 2160 <template x-if="walletVerified"> 2161 <div class="setup-status">Recovery phrase imported</div> 2162 </template> 2163 </div> 2164 </div> 2165 </div> 2166 <div class="setup-actions"> 2167 <button class="primary" type="button" :disabled="!setupCanContinue()" @click="completeSetup">Continue</button> 2168 </div> 2169 </section> 2170 </div> 2171 </body> 2172 </html>"# 2173 ); 2174 2175 #[cfg(test)] 2176 mod tests { 2177 use super::INDEX_HTML; 2178 2179 #[test] 2180 fn app_javascript_cachebuster_matches_package_version() { 2181 assert!(INDEX_HTML.contains(concat!( 2182 r#"<script defer src="/assets/iuna-ui.js?v="#, 2183 env!("CARGO_PKG_VERSION"), 2184 r#""></script>"# 2185 ))); 2186 } 2187 2188 #[test] 2189 fn wallet_endpoint_is_public_only_and_precedes_stratum() { 2190 let wallet = INDEX_HTML.find("Wallet endpoint").expect("wallet endpoint"); 2191 let stratum = INDEX_HTML 2192 .find("Stratum endpoint") 2193 .expect("stratum endpoint"); 2194 2195 assert!(wallet < stratum); 2196 assert!( 2197 INDEX_HTML 2198 .contains(r#"<div class="panel" x-show="advancedMode() && p2pAcceptInbound">"#) 2199 ); 2200 assert!(!INDEX_HTML.contains(r#"x-text="walletEndpointRestartMessage()""#)); 2201 assert!(!INDEX_HTML.contains(r#"x-text="stratumRestartMessage()""#)); 2202 } 2203 2204 #[test] 2205 fn migration_panel_is_only_visible_while_legacy_utxos_remain() { 2206 assert!(INDEX_HTML.contains( 2207 r#"x-show="status.quantum_migration?.active && status.quantum_migration?.legacy_utxos > 0""# 2208 )); 2209 assert!(!INDEX_HTML.contains( 2210 r#"status.quantum_migration?.legacy_balance > 0 || status.quantum_migration?.hybrid_balance > 0"# 2211 )); 2212 } 2213 2214 #[test] 2215 fn receive_panel_presents_the_current_rotating_address() { 2216 assert!(INDEX_HTML.contains(r#"@click="copyReceiveAddress""#)); 2217 assert!(INDEX_HTML.contains(r#"x-text="receiveAddress()""#)); 2218 assert!(INDEX_HTML.contains( 2219 "A new receive address is selected after funds are received. Previous addresses remain monitored by this wallet." 2220 )); 2221 assert!(INDEX_HTML.contains(r#"x-text="walletAddressSummary()""#)); 2222 assert!(INDEX_HTML.contains(r#"x-show="showWalletAddresses""#)); 2223 assert!(INDEX_HTML.contains("Funded wallet addresses")); 2224 assert!(!INDEX_HTML.contains("copyWalletAddress")); 2225 assert!( 2226 !INDEX_HTML 2227 .contains("Do not reuse this address after its key has been revealed by a spend.") 2228 ); 2229 } 2230 }