diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index 49be864f..bde54a01 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -1,8 +1,9 @@ -/* Igneum Miner dashboard (miner-ui-2). The site's tokens (site/index.html): obsidian, graphite, ember, molten, bone, - ash; Unbounded for headings, IBM Plex Sans for text, IBM Plex Mono for numbers and labels. Fonts ship in the binary. - One type scale (--t-*), one spacing scale (--s-*), one colour set (:root), one accent (ember), used by every page. - Layout: a rail on the left (six sections), a thin top bar, the status strip under it, the page in the middle, the - log drawer at the bottom. The window lays out from 900 x 600 up (the hosts say the same minimum). +/* Igneum Miner dashboard (miner-ui-3). The site's tokens (site/index.html): obsidian, graphite, ember, molten, bone, + ash; Unbounded for the page title, the big button and the hero numbers; IBM Plex Sans for words; IBM Plex Mono for + labels, units and small numbers. Fonts ship in the binary. One type scale (--t-*), one spacing scale (--s-*), one + accent (ember). Dark by default, light under prefers-color-scheme or [data-theme=light]. Layout: a rail on the left + (four sections), a thin top bar, the status strip under it, the page in the middle, the log drawer at the bottom. + The window lays out from 900 x 600 (the hosts' minimum); under 720 px the rail becomes a bottom tab bar. Nothing here is newer than 2022 CSS (the WebView2 host). */ @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexMono-400.woff2) format('woff2')} @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/IBMPlexMono-500.woff2) format('woff2')} @@ -14,36 +15,42 @@ @font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(fonts/Unbounded-900.woff2) format('woff2')} :root{ - /* colour */ - --obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--ember-hi:#FF6A2B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E; - --ember-12:rgba(242,84,27,.12);--ember-40:rgba(242,84,27,.4);--molten-10:rgba(255,179,92,.1);--molten-40:rgba(255,179,92,.4);--node-blue:#7FA7C9;--nvidia:#8BE37A;--rail-bg:#111114; + /* colour, dark */ + --obsidian:#0C0C0E;--graphite:#16161A;--row:#111114;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--ember-hi:#FF6A2B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E; + --ember-12:rgba(242,84,27,.12);--ember-40:rgba(242,84,27,.4);--molten-10:rgba(255,179,92,.1);--molten-40:rgba(255,179,92,.4);--nvidia:#8BE37A;--rail-bg:#111114;--hover:rgba(255,255,255,.04);--top-bg:rgba(12,12,14,.86);--shadow:rgba(0,0,0,.6);--scrim:rgba(12,12,14,.72);--ok:#FFB35C; /* type scale */ - --t-xs:11px;--t-sm:12px;--t-base:13px;--t-md:14px;--t-lg:15px;--t-xl:16px;--t-2xl:18px;--t-num:26px;--t-h3:16px;--t-h2:32px;--t-h1:52px; + --t-xs:11px;--t-sm:12px;--t-base:13px;--t-md:14px;--t-lg:15px;--t-xl:17px;--t-num:24px;--t-hero:34px;--t-h2:28px;--t-h1:44px; /* spacing scale */ - --s-1:4px;--s-2:8px;--s-3:12px;--s-4:16px;--s-5:20px;--s-6:28px;--s-7:40px; - --gutter:var(--s-6);--card-pad:22px;--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:var(--s-5);--gap-tile:var(--s-3); + --s-1:4px;--s-2:8px;--s-3:12px;--s-4:16px;--s-5:24px;--s-6:32px; + --gutter:var(--s-6);--card-pad:var(--s-5);--card-r:16px;--row-r:12px;--gap:var(--s-4); --sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--head:'Unbounded',sans-serif; --top:60px;--bottom:0px;--drawer-h:260px;--rail:196px} +@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){ + --obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF; + --ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--nvidia:#3C9B2C;--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--ok:#B8731F}} +:root[data-theme="light"]{ + --obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF; + --ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--nvidia:#3C9B2C;--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--ok:#B8731F} *{box-sizing:border-box} html,body{height:100%} -body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:var(--t-lg);line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums} +body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:var(--t-md);line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums} .mono,code,pre{font-family:var(--mono);font-variant-numeric:tabular-nums} .dim{color:var(--ash)} h1,h2,h3{font-family:var(--head);margin:0;line-height:1.1;text-wrap:balance} h1{font-weight:900;font-size:var(--t-h1);letter-spacing:-.01em} h2{font-weight:700;font-size:var(--t-h2)} -h3{font-weight:700;font-size:var(--t-h3)} +h3{font-weight:700;font-size:var(--t-xl)} p{margin:0} a{color:inherit} button{font:inherit;color:inherit} input,textarea{font-variant-numeric:tabular-nums} -.eyebrow{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.18em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:var(--s-2);white-space:nowrap} +.eyebrow{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.16em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:var(--s-2);white-space:nowrap} .eyebrow.ember{color:var(--ember)} [hidden]{display:none !important} ::selection{background:rgba(242,84,27,.45)} /* buttons */ -.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2);min-height:44px;padding:10px 20px;border-radius:10px;font-weight:600;font-size:var(--t-lg);border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease,color .15s ease;white-space:nowrap} +.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2);min-height:40px;padding:8px 18px;border-radius:10px;font-weight:600;font-size:var(--t-md);border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease,color .15s ease;white-space:nowrap} .btn:hover{transform:translateY(-1px);border-color:var(--ash)} .btn:active{transform:none} .btn:disabled{opacity:.4;cursor:default;transform:none} @@ -68,9 +75,9 @@ body.mac .rail-brand{padding-top:30px} .rail-nav{display:flex;flex-direction:column;gap:3px} .nav{position:relative;display:flex;align-items:center;gap:12px;width:100%;min-height:42px;padding:8px 12px;border:1px solid transparent;border-radius:11px;background:transparent;color:var(--ink-2);font-weight:500;font-size:var(--t-md);text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease} .nav svg{width:19px;height:19px;flex:0 0 19px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--ash);transition:color .15s ease} -.nav:hover{background:rgba(255,255,255,.04);color:var(--bone)} +.nav:hover{background:var(--hover);color:var(--bone)} .nav:hover svg{color:var(--ink-2)} -.nav.on{background:var(--ember-12);border-color:rgba(242,84,27,.28);color:var(--bone);font-weight:600} +.nav.on{background:var(--ember-12);border-color:var(--ember-40);color:var(--bone);font-weight:600} .nav.on svg{color:var(--ember)} .nav.on::before{content:"";position:absolute;left:-13px;top:10px;bottom:10px;width:3px;border-radius:0 3px 3px 0;background:var(--ember)} .nav.small{min-height:36px;font-size:var(--t-base);color:var(--ash)} @@ -85,7 +92,7 @@ body.mac .rail-brand{padding-top:30px} .rail-version{font-size:var(--t-xs);color:var(--ash);padding:8px 12px 0;letter-spacing:.06em} /* top bar */ -.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:0 var(--gutter);background:rgba(12,12,14,.86);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid rgba(42,42,48,.7);z-index:20;-webkit-app-region:drag} +.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:0 var(--gutter);background:var(--top-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--line);z-index:20;-webkit-app-region:drag} .top button,.top .pill{-webkit-app-region:no-drag} body.mac:not(.has-rail) .top{padding-left:92px} body.has-rail .top{left:var(--rail)} @@ -96,21 +103,17 @@ body.has-rail .top{left:var(--rail)} .page-title h1{font-size:19px;font-weight:700;letter-spacing:0;white-space:nowrap} .page-sub{font-size:var(--t-base);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} .top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto;min-width:0} -.top-status{font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:380px} -.top-status .pc+.pc::before{content:"·";margin:0 7px;color:var(--line-2)} .pill{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;min-width:112px;justify-content:center} .pill.on{color:var(--molten);border-color:var(--molten-40)} .pill.warn{color:var(--ember)} .dot{width:8px;height:8px;border-radius:50%;background:var(--ash);display:inline-block;flex:0 0 8px} -.dot.small{width:7px;height:7px;flex-basis:7px} .on .dot,.dot.live{background:var(--molten);animation:pulse 2s ease-in-out infinite} -.warn .dot{background:var(--ember);animation:none} +.warn .dot,.dot.bad{background:var(--ember);animation:none} @keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,179,92,.5)}50%{box-shadow:0 0 0 7px rgba(255,179,92,0)}} @media (prefers-reduced-motion:reduce){.on .dot,.dot.live{animation:none}} /* the status strip under the top bar (app.js, Notices): one notice at a time. It takes no room while empty; main's - top moves once per change with a 150 ms transition (layoutStrip), never per poll. Tones: default molten (running, - available, done), bad ember (failed, clock block, urgent). */ + top moves once per change with a 150 ms transition (layoutStrip), never per poll. */ .notices{position:fixed;top:var(--top);left:0;right:0;z-index:19} body.has-rail .notices{left:var(--rail)} .notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-4);padding:8px calc(var(--gutter) - 6px) 8px var(--gutter);background:var(--molten-10);border-bottom:1px solid var(--molten-40);font-size:var(--t-base);line-height:1.4;color:var(--bone)} @@ -124,11 +127,18 @@ body.has-rail .notices{left:var(--rail)} .notice-close:hover{color:var(--bone);border-color:var(--line-2)} .notice.update-urgent .notice-close{color:#fff} .notice-detail{flex-basis:100%;font-size:var(--t-sm);color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-3px} -.notice .prog{flex-basis:100%;height:3px;background:rgba(255,255,255,.12);border-radius:2px;overflow:hidden;margin-top:-3px} +.notice .prog{flex-basis:100%;height:3px;background:rgba(127,127,127,.2);border-radius:2px;overflow:hidden;margin-top:-3px} .notice .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear} +/* a question in place of a dialog: the quit strip over the page, the inline asks inside a card */ +.ask-wrap{position:fixed;left:0;right:0;bottom:0;z-index:36;display:flex;justify-content:center;padding:0 var(--s-4) var(--s-4);pointer-events:none} +body.has-rail .ask-wrap{left:var(--rail)} +.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--graphite);border:1px solid var(--ember-40);border-radius:14px;padding:12px 16px;box-shadow:0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease} +.ask-text{flex:1 1 260px;font-size:var(--t-md);min-width:0} +.ask.inline{box-shadow:none;background:var(--ember-12);margin-top:var(--s-3);animation:none} + /* screens and pages */ -main{position:absolute;top:var(--top);bottom:0;left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease} +main{position:absolute;top:var(--top);bottom:var(--bottom);left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease} @media (prefers-reduced-motion:reduce){main{transition:none}} body.has-rail main{left:var(--rail)} body.drawer-open main{bottom:var(--drawer-h)} @@ -140,15 +150,14 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .page{display:flex;flex-direction:column;gap:var(--gap);animation:rise .3s ease} /* welcome */ -.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--s-4);padding:var(--s-7) 0 48px} -.coin-wrap{position:relative;width:148px;height:148px;margin-bottom:6px} -.coin-wrap::before{content:"";position:absolute;inset:-40px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.28) 0,rgba(242,84,27,0) 65%);animation:breathe 4s ease-in-out infinite} -.coin{position:relative;display:block;border-radius:50%;filter:drop-shadow(0 10px 30px rgba(242,84,27,.35))} +.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--s-4);padding:var(--s-6) 0 48px} +.mark-wrap{position:relative;width:120px;height:120px;border-radius:26px;background:#0C0C0E;display:flex;align-items:center;justify-content:center;margin-bottom:6px;box-shadow:0 20px 50px rgba(242,84,27,.25)} +.mark-wrap::before{content:"";position:absolute;inset:-40px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.28) 0,rgba(242,84,27,0) 65%);animation:breathe 4s ease-in-out infinite;z-index:-1} @keyframes breathe{0%,100%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}} -@media (prefers-reduced-motion:reduce){.coin-wrap::before{animation:none}} -.lead{font-size:var(--t-2xl);color:var(--ink-2);max-width:54ch} -.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap-tile);width:100%;max-width:860px;margin-top:10px;text-align:left} -.tile{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);display:flex;flex-direction:column;gap:6px;min-width:0} +@media (prefers-reduced-motion:reduce){.mark-wrap::before{animation:none}} +.lead{font-size:var(--t-xl);color:var(--ink-2);max-width:54ch} +.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-3);width:100%;max-width:860px;margin-top:10px;text-align:left} +.tile{background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;flex-direction:column;gap:6px;min-width:0} .tile .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;color:var(--ember)} .tile .t{font-family:var(--head);font-weight:700;font-size:var(--t-lg);line-height:1.25} .tile .s{font-size:var(--t-base);color:var(--ash);line-height:1.45} @@ -162,6 +171,9 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .note{font-size:var(--t-base);color:var(--ash);line-height:1.5} .note.small{font-size:var(--t-sm);word-break:break-all} .help{font-size:var(--t-base);color:var(--ash);line-height:1.5;max-width:64ch} +.line-text{font-size:var(--t-md);color:var(--ink-2);line-height:1.5} +.line-text.ok{color:var(--molten)} +.line-text.bad{color:var(--ember)} .cards{display:flex;flex-direction:column;gap:var(--s-3);margin-top:var(--s-2)} .card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0} .card.detecting{display:flex;align-items:center;gap:18px} @@ -169,11 +181,13 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .card.detecting .s{font-size:var(--t-sm);color:var(--ash);margin-top:var(--s-1)} .spinner{width:28px;height:28px;border-radius:50%;border:3px solid var(--line-2);border-top-color:var(--ember);animation:spin 1s linear infinite;flex:0 0 28px} @keyframes spin{to{transform:rotate(360deg)}} +.top-gap{margin-top:var(--s-4)} +.indent{margin-left:52px} /* GPU rows (first run) */ .gpu-row{display:flex;align-items:center;gap:var(--s-4);background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0;flex-wrap:wrap} .gpu-row.off{opacity:.72} -.badge{width:48px;height:48px;border-radius:13px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 48px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} +.badge{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 44px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} .badge.apple{color:var(--bone)} .badge.nvidia{color:var(--nvidia)} .badge.amd{color:var(--ember)} @@ -183,17 +197,15 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .kind.discrete,.kind.apple{color:var(--molten);border-color:var(--molten-40)} .kind.external{color:var(--bone)} .gpu-row .meta{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 14px} -.gpu-row .meta b{color:var(--ink-2);font-weight:500} .gpu-row .reason{font-size:var(--t-sm);color:var(--ash)} .gpu-row .msg{font-size:var(--t-sm);color:var(--ember)} .gpu-row .switch{padding:0;flex:0 0 auto;margin-left:auto} -/* hot-plug (src/hotplug.rs): a removed card dims, a faulty one is named in ember, neither has live controls */ -.gpu-row.removed,.gpu-line.removed,.set-card.removed{opacity:.5} -.gpu-row.unusable .name,.gpu-line.unusable .name,.set-card.unusable .name{color:var(--ember)} +.gpu-row.removed,.gpu-line.removed{opacity:.5} +.gpu-row.unusable .name,.gpu-line.unusable .name{color:var(--ember)} -/* the Mine page: the big switch and the three numbers */ -.hero-row{display:grid;grid-template-columns:1.3fr 1fr 1fr 1fr;gap:var(--gap-tile)} -.toggle-big{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:4px;min-height:112px;padding:18px 20px;border-radius:var(--tile-r);border:1px solid var(--ember);background:var(--ember);color:var(--ember-ink);cursor:pointer;text-align:left;transition:background .15s ease,border-color .15s ease,transform .15s ease,color .15s ease;min-width:0} +/* the Mine page: the big button and the live total */ +.hero-row{display:grid;grid-template-columns:minmax(220px,1fr) 2fr;gap:var(--s-3)} +.toggle-big{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:4px;min-height:112px;padding:18px 20px;border-radius:14px;border:1px solid var(--ember);background:var(--ember);color:var(--ember-ink);cursor:pointer;text-align:left;transition:background .15s ease,border-color .15s ease,transform .15s ease,color .15s ease;min-width:0} .toggle-big:hover{background:var(--ember-hi);border-color:var(--ember-hi);transform:translateY(-1px)} .toggle-big:disabled{opacity:.45;cursor:default;transform:none} .toggle-big .ring{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:rgba(12,12,14,.18);margin-bottom:6px} @@ -201,64 +213,50 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .toggle-big .tl{font-family:var(--head);font-weight:700;font-size:18px;line-height:1.15;white-space:nowrap} .toggle-big .ts{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.06em;opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%} .toggle-big.stop{background:var(--graphite);border-color:var(--line-2);color:var(--bone)} -.toggle-big.stop:hover{border-color:var(--ash);background:#1b1b20} +.toggle-big.stop:hover{border-color:var(--ash);background:var(--row)} .toggle-big.stop .ring{background:var(--ember-12);color:var(--ember)} .toggle-big.stop .ts{color:var(--molten);opacity:1} -.strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap-tile)} -.strip.three{grid-template-columns:repeat(3,minmax(0,1fr))} -.cell{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);display:flex;flex-direction:column;gap:6px;min-width:0} -.cell .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash);white-space:nowrap} -.cell .v{font-family:var(--head);font-weight:700;font-size:var(--t-num);line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:baseline;gap:var(--s-2);min-height:1.1em} -.cell.ember .v{color:var(--ember)} -.cell .v .unit{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);font-weight:500;letter-spacing:.08em} -.cell .v.state{text-transform:capitalize;font-size:22px} -.cell .v.small{font-size:18px} -.cell .v.dim{color:var(--ash)} -.cell .s{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.cell.ok .v.state{color:var(--molten)} -.cell.bad .v.state{color:var(--ember)} -.cell.bad .s{color:var(--ember)} -.grid2{display:grid;grid-template-columns:1fr 1fr;gap:var(--gap);align-items:start} +.totals{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--s-3);background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;min-width:0} +.tot{display:flex;flex-direction:column;min-width:0;padding-left:var(--s-4);border-left:1px solid var(--line)} +.tot:first-child{padding-left:0;border-left:0} +.tot .v{font-family:var(--head);font-weight:700;font-size:var(--t-hero);line-height:1.1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-.01em} +.tot .v.ember{color:var(--ember)} +.tot .v.ask-price{font-family:var(--sans);font-size:var(--t-md);font-weight:600;color:var(--molten);text-decoration:underline;text-underline-offset:3px;cursor:pointer;padding:9px 0 8px} +.tot .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--ash);margin-top:4px} +.tot .s{font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px} .card-head{display:flex;justify-content:space-between;align-items:center;gap:var(--s-3);margin-bottom:var(--s-3);flex-wrap:wrap} -.stats{display:flex;flex-wrap:wrap;gap:12px 16px;font-size:var(--t-sm);color:var(--ash)} -.stats b{color:var(--bone);font-weight:500;font-variant-numeric:tabular-nums} -#dag{display:block;width:100%;height:170px;border-radius:12px;background:var(--obsidian)} -.legend{display:flex;flex-wrap:wrap;gap:14px 18px;margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ink-2)} -.legend span{display:inline-flex;align-items:center;gap:var(--s-2)} -.sw{width:12px;height:12px;border-radius:3px;display:inline-block;border:1px solid var(--line-2)} -.sw.ember{background:var(--ember);border-color:var(--ember)} -.sw.glow{border-color:var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)} -.sw.line{width:18px;height:0;border:0;border-top:1px dashed var(--line-2);border-radius:0} +.head-right{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;min-width:0} +.stats{font-size:var(--t-sm);color:var(--ash)} +#dag{display:block;width:100%;height:110px;border-radius:10px;background:var(--obsidian);margin-bottom:var(--s-3)} .empty{color:var(--ash);font-size:var(--t-base);padding:10px 0} .empty.err{color:var(--ember)} .kv{display:flex;flex-direction:column} -.kv>div{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s-3);padding:7px 0;border-bottom:1px solid var(--line)} +.kv>div{display:grid;grid-template-columns:120px auto 1fr;align-items:baseline;gap:var(--s-3);padding:7px 0;border-bottom:1px solid var(--line)} .kv>div:last-child{border-bottom:0} .kv .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.1em;text-transform:uppercase;color:var(--ash);white-space:nowrap} -.kv .v{font-size:var(--t-md);font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right} +.kv .v{font-size:var(--t-md);font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.kv .v.ok{color:var(--molten)} .kv .v.warn{color:var(--ember)} +.kv .v.dim{color:var(--ash)} +.kv .m{font-size:var(--t-sm);color:var(--ash);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .card .note{margin-top:10px} .card .help+.help{margin-top:6px} -.big-word{font-family:var(--head);font-weight:700;font-size:20px;line-height:1.2;margin:2px 0 8px;word-break:break-word} -.big-word.ok{color:var(--molten)} -.big-word.warn{color:var(--ember)} -.big-word.dim{color:var(--ash)} -.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);max-height:260px;overflow:auto} -.feed>div{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:8px 0;align-items:baseline} +.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2)} +.feed>div{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:7px 0;align-items:baseline} .feed>div:last-child{border-bottom:0} -.feed>div>span:first-child{min-width:0} +.feed>div>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .feed .t{color:var(--ash);flex:0 0 auto;font-size:var(--t-xs)} .feed .k{color:var(--molten);margin-right:6px} -.feed .k.error{color:var(--ember)} -.feed .k.warn{color:var(--ember)} -.feed .k.block{color:var(--ember)} -.feed .k.build{color:var(--ink-2)} +.feed .k.error,.feed .k.warn,.feed .k.block{color:var(--ember)} +.feed .k.build,.feed .k.info{color:var(--ash)} -/* the GPU rows on the Mine page: badge, name, numbers, the switch */ +/* the card rows on Mine (the hero object): badge, name and state, the numbers, Tune, the switch, the chevron; + the tune line under; the details under that */ .gpu-list{display:flex;flex-direction:column;gap:var(--s-2)} -.gpu-line{display:grid;grid-template-columns:44px minmax(160px,1.4fr) repeat(3,minmax(84px,.7fr)) 48px;align-items:center;gap:var(--s-4);padding:12px 14px;border:1px solid var(--line);border-radius:var(--tile-r);background:var(--obsidian);min-width:0} +.gpu-line{border:1px solid var(--line);border-radius:var(--row-r);background:var(--row);min-width:0} .gpu-line.off{opacity:.62} -.gpu-line .badge{width:44px;height:44px;flex-basis:44px;border-radius:12px} +.gpu-line.open{border-color:var(--line-2)} +.gl-main{display:grid;grid-template-columns:44px minmax(150px,1.2fr) auto auto;align-items:center;gap:var(--s-4);padding:12px 14px} .gpu-line .who{min-width:0;display:flex;flex-direction:column;gap:3px} .gpu-line .name{font-family:var(--head);font-weight:700;font-size:var(--t-md);line-height:1.25;display:flex;align-items:center;gap:8px;flex-wrap:wrap} .gpu-line .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} @@ -267,46 +265,75 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .gpu-line .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block;flex:0 0 7px} .gpu-line .msg{font-size:var(--t-sm);color:var(--ember);line-height:1.4} .gpu-line .msg.dim{color:var(--ash)} -.gpu-line .num{display:flex;flex-direction:column;gap:2px;min-width:0} -.gpu-line .num .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);white-space:nowrap} -.gpu-line .num .v{font-family:var(--head);font-weight:700;font-size:18px;line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:baseline;gap:5px} -.gpu-line .num .v .unit{font-family:var(--mono);font-size:10px;color:var(--ash);font-weight:500;letter-spacing:.08em} -.gpu-line .num .v.hot{color:var(--ember)} -.gpu-line .num .v.warm{color:var(--molten)} -.gpu-line .num .v.na{color:var(--ash);font-weight:500;font-size:var(--t-md)} +.nums{display:flex;gap:var(--s-5);justify-content:flex-end;align-items:flex-start;min-width:0;flex-wrap:wrap} +.num{display:flex;flex-direction:column;gap:1px;min-width:56px;align-items:flex-end;text-align:right} +.num .v{font-family:var(--head);font-weight:700;font-size:20px;line-height:1.15;white-space:nowrap;display:flex;align-items:baseline;gap:4px} +.num .v .unit{font-family:var(--mono);font-size:10px;color:var(--ash);font-weight:500;letter-spacing:.08em} +.num .k{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--ash);white-space:nowrap} +.num .v.hot{color:var(--ember)} +.num .v.warm{color:var(--molten)} .gpu-line.on .num.hash .v{color:var(--ember)} -.gpu-line .switch{padding:0;justify-self:end} - -/* the Settings page: per-card controls */ -.set-cards{display:flex;flex-direction:column;gap:var(--s-3);margin-bottom:var(--s-3)} -.set-card{border:1px solid var(--line);border-radius:var(--tile-r);background:var(--obsidian);padding:14px 16px;display:flex;flex-direction:column;gap:10px;min-width:0} -.set-card .head{display:flex;align-items:center;gap:10px;flex-wrap:wrap} -.set-card .head .name{font-family:var(--head);font-weight:700;font-size:var(--t-md)} -.set-card .head .meta{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash)} -.set-card .ctl{display:grid;grid-template-columns:150px 1fr auto;align-items:center;gap:var(--s-3)} -.set-card .ctl .k{font-size:var(--t-md);color:var(--bone);font-weight:500} -.set-card .ctl .pv{font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);min-width:110px;text-align:right;white-space:nowrap} -.set-card input[type=range]{width:100%;accent-color:var(--ember);margin:0} -.set-card .ctl .help{grid-column:1 / -1;margin-top:-4px} -.set-card .line{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center} -.set-card .line b{color:var(--ink-2);font-weight:500} -.set-card .line.ok{color:var(--molten)} -.set-card .line.hot{color:var(--ember)} -.set-card .line.on{color:var(--molten)} -.set-card .line .pin{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line-2);border-radius:6px;padding:1px 6px} +.acts{display:flex;align-items:center;gap:var(--s-3);justify-self:end} +.acts .switch{padding:0} +.chev-btn{width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ash)} +.chev-btn:hover{border-color:var(--line-2);color:var(--bone)} +.chev{width:9px;height:9px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg) translateY(-2px);transition:transform .15s ease;display:inline-block} +.open .chev,.chev-btn.open .chev{transform:rotate(225deg) translateY(-2px)} +.gl-tune{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;padding:8px 14px 10px 72px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);border-top:1px solid var(--line)} +.gl-tune .t{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} +.gl-tune .n{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.gl-tune.running{color:var(--molten)} +.gl-tune.stopped,.gl-tune.paused{color:var(--ember)} +.gl-tune .bar{flex-basis:100%;height:3px;border-radius:2px;background:var(--line);overflow:hidden;display:block} +.gl-tune .bar b{display:block;height:100%;background:var(--molten);transition:width .5s linear} +.gl-details{padding:12px 14px 14px 72px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:10px;user-select:text;-webkit-user-select:text} +.gl-details .ctl{display:grid;grid-template-columns:120px 1fr auto;align-items:center;gap:var(--s-3)} +.gl-details .ctl .k{font-size:var(--t-md);color:var(--bone);font-weight:500} +.gl-details .ctl .pv{font-size:var(--t-sm);color:var(--ink-2);min-width:100px;text-align:right;white-space:nowrap} +.gl-details input[type=range]{width:100%;accent-color:var(--ember);margin:0} +.gl-details .ctl .help{grid-column:1 / -1;margin-top:-4px} +.gl-details .ctl .help:first-of-type{grid-column:2} +.gl-details .line{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:6px 12px;align-items:center} +.gl-details .line b{color:var(--ink-2);font-weight:500} +.gl-details .line.ok{color:var(--molten)} +.gl-details .line.hot{color:var(--ember)} +.gl-details .line .pin{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line-2);border-radius:6px;padding:1px 6px} +.curve{font-size:var(--t-sm);font-family:var(--mono);border-collapse:collapse;width:100%} +.curve th{text-align:left;font-weight:500;color:var(--ash);font-size:10px;letter-spacing:.1em;text-transform:uppercase;padding:3px 8px 3px 0} +.curve td{padding:3px 8px 3px 0;border-top:1px solid var(--line);color:var(--ink-2)} +.curve tr.marked td{color:var(--ash);text-decoration:line-through} .ids{display:flex;align-items:center;gap:6px;flex:0 0 auto;justify-self:end} .ids button{width:30px;height:30px;border-radius:8px;border:1px solid var(--line-2);background:transparent;color:var(--bone);cursor:pointer;font-size:16px;line-height:1} .ids button:hover{border-color:var(--ash)} .ids input{width:44px;text-align:center;background:var(--obsidian);border:1px solid var(--line-2);border-radius:8px;padding:5px 4px;color:var(--bone);font-family:var(--mono);font-size:var(--t-base);user-select:text;-webkit-user-select:text} .ids input:focus{outline:none;border-color:var(--ember)} .ids input::-webkit-inner-spin-button,.ids input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} -.lead-card .lead-row,.lead-row{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s-4)} -.lead-text{min-width:0;display:flex;flex-direction:column;gap:6px} -.lead-text h3{font-size:var(--t-2xl)} + +/* the tuning card: the goal, its consequence, the schedule, Power control when it is the fix */ +.goal-row{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap;margin-bottom:var(--s-3)} +.seg{display:inline-flex;background:var(--obsidian);border:1px solid var(--line);border-radius:10px;padding:3px;gap:2px} +.seg-b{border:0;background:transparent;color:var(--ash);font-size:var(--t-base);font-weight:500;padding:6px 14px;border-radius:8px;cursor:pointer;transition:background .15s ease,color .15s ease;white-space:nowrap} +.seg-b:hover{color:var(--bone)} +.seg-b.on{background:var(--graphite);color:var(--bone);box-shadow:0 1px 3px rgba(0,0,0,.25);font-weight:600} +.goal-line{font-size:var(--t-base);color:var(--ink-2);min-width:0} + +/* the node line and the disclosures */ +.disclose{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);width:100%;border:0;background:transparent;padding:0;cursor:pointer;text-align:left;color:var(--bone);font-size:var(--t-md);font-weight:500;min-height:32px} +.disclose:hover{color:var(--bone)} +.disclose .chev{color:var(--ash);flex:0 0 auto;margin-right:4px} +.disclose-right{display:flex;align-items:center;gap:var(--s-3);min-width:0} +.disclose-right .dim{font-size:var(--t-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:52ch} +.node-line{display:inline-flex;align-items:center;gap:10px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.node-card.bad .node-line{color:var(--ember)} +.card .disclose+.disclose,.card .details+.disclose,.card .help+.disclose,.card .addr-big+.help+.disclose{margin-top:var(--s-3);padding-top:var(--s-3);border-top:1px solid var(--line)} +.details{padding-top:var(--s-3);display:flex;flex-direction:column;gap:var(--s-3)} .switch-list{display:flex;flex-direction:column;gap:3px;font-size:var(--t-sm);color:var(--ash);margin-top:8px} .switch-list span{display:flex;justify-content:space-between;gap:12px} .switch-list span.past{opacity:.55} .switch-list span.next{color:var(--molten)} +.lead-row{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s-4)} +.lead-text{min-width:0;display:flex;flex-direction:column;gap:6px} +.lead-text h3{font-size:var(--t-xl)} .addr-big{display:flex;align-items:center;gap:12px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:12px;padding:14px 16px;font-size:var(--t-lg);word-break:break-all;user-select:text;-webkit-user-select:text;margin-bottom:10px} .addr-big span{flex:1;min-width:0;color:var(--molten)} .card.adv{padding:0} @@ -317,22 +344,35 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .card.adv summary .eyebrow{margin-left:0} .card.adv>:not(summary){margin-left:var(--card-pad);margin-right:var(--card-pad)} .card.adv>:last-child{margin-bottom:var(--card-pad)} -.job-history table{margin-top:var(--s-2)} table{border-collapse:collapse;width:100%;font-size:var(--t-base)} -.job-history th{text-align:left;font-weight:500;color:var(--ash);font-size:var(--t-xs);padding:4px 6px 4px 0;font-family:var(--mono);letter-spacing:.1em;text-transform:uppercase} -.job-history td{padding:6px 6px 6px 0;border-top:1px solid var(--line);vertical-align:top} -.job-history tr.failed td,.job-history tr.timeout td,.job-history tr.aborted td{color:var(--ember)} -.job-history tr.running td{color:var(--molten)} -.clock-card{border:1px solid rgba(242,84,27,.5);background:rgba(242,84,27,.08);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)} -.clock-card.warn{border-color:var(--molten-40);background:rgba(255,179,92,.06)} +#s-jobs-history th{text-align:left;font-weight:500;color:var(--ash);font-size:var(--t-xs);padding:4px 6px 4px 0;font-family:var(--mono);letter-spacing:.1em;text-transform:uppercase} +#s-jobs-history td{padding:6px 6px 6px 0;border-top:1px solid var(--line);vertical-align:top} +#s-jobs-history tr.failed td,#s-jobs-history tr.timeout td,#s-jobs-history tr.aborted td{color:var(--ember)} +#s-jobs-history tr.running td{color:var(--molten)} +.clock-card{border:1px solid var(--ember-40);background:var(--ember-12);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)} +.clock-card.warn{border-color:var(--molten-40);background:var(--molten-10)} .clock-msg{font-size:var(--t-md);color:var(--bone);line-height:1.45} .clock-card .note{margin-top:0} +/* earnings: money first */ +.money{display:flex;flex-direction:column;margin-bottom:var(--s-2)} +.money-row{display:flex;align-items:baseline;gap:var(--s-3);padding:8px 0;border-bottom:1px solid var(--line);flex-wrap:wrap} +.money-row:last-child{border-bottom:0} +.money-row .v{font-family:var(--head);font-weight:700;font-size:var(--t-hero);line-height:1.1;white-space:nowrap;letter-spacing:-.01em;min-width:200px} +.money-row .v.small{font-size:var(--t-num)} +.money-row .s{font-size:var(--t-base);color:var(--ash);min-width:0} + +/* prove */ +.tier-list{list-style:none;margin:var(--s-3) 0 0;padding:0;display:flex;flex-direction:column;gap:6px;font-size:var(--t-md);color:var(--ink-2)} +.tier-list li{display:flex;gap:10px;align-items:baseline} +.tier-list li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ember);flex:0 0 6px;position:relative;top:-2px} +#pv-line{margin-top:var(--s-3)} + /* address options */ .options{display:flex;flex-direction:column;gap:var(--s-3);margin-top:6px} .option{display:flex;gap:var(--s-4);align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease} .option:hover{border-color:var(--line-2)} -.option.on{border-color:var(--ember);background:#1A1614} +.option.on{border-color:var(--ember);background:var(--ember-12)} .option input[type=radio]{position:absolute;opacity:0;width:0;height:0} .option .radio{width:20px;height:20px;border-radius:50%;border:2px solid var(--line-2);flex:0 0 20px;margin-top:2px;position:relative} .option.on .radio{border-color:var(--ember)} @@ -342,15 +382,15 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} .option .s{font-size:var(--t-md);color:var(--ash);line-height:1.5} .tag{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--molten);border:1px solid var(--molten-40);border-radius:999px;padding:2px 8px} .addr-input{width:100%;margin-top:var(--s-2);background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;color:var(--bone);font-size:var(--t-md);letter-spacing:.02em;user-select:text;-webkit-user-select:text} +.addr-input.short{width:100px;flex:0 0 100px} .addr-input:focus{outline:none;border-color:var(--ember)} .option:not(.on) .addr-input{display:none} .err{font-size:var(--t-base);color:var(--ember)} -/* the update card (app.js, UpdateCard; the wallet carries the same block): the mark with its ring, the name, one - line, up to three note lines, the size, Install now and Later. Over the key sheet and the pages. */ -.upd-wrap{position:fixed;inset:0;z-index:35;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease} +/* the update card (app.js, UpdateCard; the wallet carries the same block) */ +.upd-wrap{position:fixed;inset:0;z-index:35;background:var(--scrim);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease} @keyframes fade{from{opacity:0}to{opacity:1}} -.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--s-2);box-shadow:0 30px 80px rgba(0,0,0,.6),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none} +.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--s-2);box-shadow:0 30px 80px var(--shadow),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none} .upd-card::before{content:"";position:absolute;left:50%;top:-80px;width:360px;height:260px;transform:translateX(-50%);border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.22) 0,rgba(242,84,27,0) 62%);pointer-events:none} .upd-mark{position:relative;width:96px;height:96px;display:flex;align-items:center;justify-content:center;margin-bottom:var(--s-3)} .upd-mark img{position:relative;display:block;filter:drop-shadow(0 6px 18px rgba(242,84,27,.35))} @@ -381,8 +421,8 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} @media (max-height:620px){.upd-card{padding:24px 24px 20px}.upd-mark{width:72px;height:72px;margin-bottom:var(--s-2)}.upd-mark img{width:32px;height:32px}.upd-name{font-size:20px}} /* sheet (the key) */ -.sheet-wrap{position:fixed;inset:0;z-index:30;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px} -.sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;max-height:calc(100vh - 48px);overflow:auto;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px rgba(0,0,0,.6);animation:rise .3s ease} +.sheet-wrap{position:fixed;inset:0;z-index:30;background:var(--scrim);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px} +.sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;max-height:calc(100vh - 48px);overflow:auto;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px var(--shadow);animation:rise .3s ease} .sheet .sub{font-size:var(--t-lg);color:var(--ink-2)} .field{display:flex;flex-direction:column;gap:var(--s-2);margin-top:6px} .field .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} @@ -398,13 +438,13 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} /* rows, switches */ .row{display:flex;gap:10px;align-items:center;min-width:0} -.row.between{justify-content:space-between} .row.wrap{flex-wrap:wrap} .row .addr-input{margin-top:0;min-width:0} -.switch{display:flex;align-items:center;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:8px 0 2px;line-height:1.35} +.switch{display:flex;align-items:flex-start;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:9px 0;line-height:1.4} +.switch+.switch{border-top:1px solid var(--line)} .switch input{position:absolute;opacity:0;width:0;height:0} -.switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease} -.switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--bone);transition:transform .15s ease} +.switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease;margin-top:1px} +.switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#F4F1EC;transition:transform .15s ease} .switch input:checked+.track{background:var(--ember)} .switch input:checked+.track::after{transform:translateX(18px)} .switch input:focus-visible+.track{outline:2px solid var(--ember);outline-offset:3px} @@ -412,10 +452,12 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} .switch.lg .track{width:52px;height:30px;flex-basis:52px} .switch.lg .track::after{width:24px;height:24px} .switch.lg input:checked+.track::after{transform:translateX(22px)} -.switch+.help{margin-bottom:6px} +.sw-text{min-width:0} +.sw-text b{font-weight:600} +.sw-text .dim{font-size:var(--t-base)} /* log drawer */ -.drawer{position:fixed;left:0;right:0;bottom:0;height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:21;transition:height .2s ease;display:flex;flex-direction:column;box-shadow:0 -12px 30px rgba(0,0,0,.35)} +.drawer{position:fixed;left:0;right:0;bottom:0;height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:21;transition:height .2s ease;display:flex;flex-direction:column;box-shadow:0 -12px 30px var(--shadow)} body.has-rail .drawer{left:var(--rail)} body.drawer-open .drawer{height:var(--drawer-h)} body.drawer-drag .drawer{transition:none} @@ -450,8 +492,8 @@ body.drawer-drag main{pointer-events:none} .log-ruler .t1{right:0;border-radius:0 0 0 6px} .log-ruler .mk{position:absolute;top:4px;bottom:4px;width:2px;margin-left:-1px;background:var(--ember);opacity:.9;pointer-events:none} .log-ruler .mk.swap{background:var(--molten)} -.log-ruler .win{position:absolute;top:0;bottom:0;background:rgba(244,241,236,.14);border-left:1px solid rgba(244,241,236,.35);border-right:1px solid rgba(244,241,236,.35);min-width:3px;pointer-events:none} -.log-ruler:hover .win{background:rgba(244,241,236,.2)} +.log-ruler .win{position:absolute;top:0;bottom:0;background:rgba(127,127,127,.14);border-left:1px solid rgba(127,127,127,.35);border-right:1px solid rgba(127,127,127,.35);min-width:3px;pointer-events:none} +.log-ruler:hover .win{background:rgba(127,127,127,.2)} .log-view{position:relative;flex:1;overflow:auto;overscroll-behavior:contain;user-select:text;-webkit-user-select:text;contain:strict} .log-pad{position:relative;min-height:100%} .log{margin:0;position:absolute;top:0;left:0;right:0;padding:6px var(--gutter);font-size:var(--t-sm);line-height:19px;color:var(--ink-2);white-space:pre;will-change:transform} @@ -459,7 +501,7 @@ body.drawer-drag main{pointer-events:none} .log-view.wrap .log{position:relative;padding-bottom:12px} .log-view.wrap .log .ln{height:auto;white-space:pre-wrap;word-break:break-all;overflow:visible} .log .src{color:var(--ash)} -.log .src.node{color:var(--node-blue)} +.log .src.node{color:#7FA7C9} .log .src.miner1,.log .src.miner2,.log .src.miner3,.log .src.miner4,.log .src.miner5,.log .src.miner6,.log .src.miner7,.log .src.miner8{color:var(--molten)} .log .src.app{color:var(--ember)} .log .src.watch{color:var(--ash)} @@ -467,22 +509,18 @@ body.drawer-drag main{pointer-events:none} .log .ln.hit{background:rgba(255,179,92,.18);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)} .log mark{background:rgba(255,179,92,.3);color:var(--bone);border-radius:2px;padding:0 1px} .log-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--ash);font-size:var(--t-sm);padding:0 var(--gutter);text-align:center} -.log-jump{position:absolute;right:calc(var(--gutter) + 14px);bottom:14px;background:var(--graphite);border-color:var(--molten-40);color:var(--molten);box-shadow:0 8px 24px rgba(0,0,0,.5);z-index:2;gap:6px;padding:6px 12px} -.log-jump:hover{background:#1c1a18;border-color:var(--molten)} +.log-jump{position:absolute;right:calc(var(--gutter) + 14px);bottom:14px;background:var(--graphite);border-color:var(--molten-40);color:var(--molten);box-shadow:0 8px 24px var(--shadow);z-index:2;gap:6px;padding:6px 12px} +.log-jump:hover{border-color:var(--molten)} -.toast{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:var(--t-base);z-index:40;box-shadow:0 10px 30px rgba(0,0,0,.5);max-width:min(90vw,520px);text-align:center} +.toast{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:var(--t-base);z-index:40;box-shadow:0 10px 30px var(--shadow);max-width:min(90vw,520px);text-align:center} body.has-rail .toast{left:calc(50% + var(--rail) / 2)} body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} -/* narrow windows (the 900 px floor): the rail folds to icons with small labels, the hero row and the strips fold to - two columns, the two-column grid to one, the GPU rows drop a number */ +/* narrow windows: 1180 the hero wraps; 1000 the rail folds to icons; 900 the row's cells wrap; 720 a bottom tab bar */ @media (max-width:1180px){ - .hero-row{grid-template-columns:1fr 1fr} - .toggle-big{grid-row:span 1} - .strip{grid-template-columns:repeat(2,minmax(0,1fr))} - .strip.three{grid-template-columns:repeat(3,minmax(0,1fr))} - .grid2{grid-template-columns:1fr} - .top-status{max-width:220px} + .hero-row{grid-template-columns:1fr} + .totals{grid-template-columns:repeat(4,minmax(0,1fr))} + .nums{gap:var(--s-4)} } @media (max-width:1000px){ :root{--rail:76px;--gutter:var(--s-5)} @@ -497,29 +535,73 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .rail-status{display:none} .rail-version{text-align:center;padding:8px 0 0;font-size:10px;white-space:nowrap} .rail-version .chain{display:none} - .gpu-line{grid-template-columns:44px minmax(140px,1.4fr) repeat(2,minmax(80px,.7fr)) 48px} - .gpu-line .num.power{display:none} .page-sub{display:none} - .top-status{display:none} - .strip.three{grid-template-columns:repeat(2,minmax(0,1fr))} - .set-card .ctl{grid-template-columns:120px 1fr auto} + .gl-main{grid-template-columns:44px minmax(140px,1fr) auto} + .nums{grid-column:2;justify-content:flex-start} + .acts{grid-column:3;grid-row:1} + .gl-tune,.gl-details{padding-left:14px} + .kv>div{grid-template-columns:110px auto 1fr} } @media (max-width:860px){ .three{grid-template-columns:1fr} h1{font-size:40px} + .totals{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:var(--s-3)} + .tot:nth-child(3){padding-left:0;border-left:0} + .money-row .v{min-width:0} } -/* short windows (the 600 px floor): tighter paddings, a lower hero, a smaller canvas, so the drawer always has room */ +@media (max-width:720px){ + :root{--rail:0px;--gutter:var(--s-4);--bottom:64px} + body.has-rail .rail{top:auto;bottom:0;left:0;right:0;width:auto;height:64px;flex-direction:row;align-items:center;border-right:0;border-top:1px solid var(--line);padding:0 var(--s-2)} + .rail-brand{display:none} + .rail-nav{flex-direction:row;flex:1;gap:0;justify-content:space-around} + .nav{min-height:56px;padding:6px 2px;font-size:10px;border-radius:10px;flex:1;max-width:120px} + .nav.on::before{display:none} + .rail-foot{margin-top:0;padding-top:0;border-top:0;flex-direction:row;gap:0} + .rail-foot .nav.small{min-height:56px;flex:0 0 auto;padding:6px 8px} + .rail-version{display:none} + body.has-rail .top,body.has-rail .notices,body.has-rail main,body.has-rail .drawer,body.has-rail .ask-wrap{left:0} + body.has-rail main{bottom:var(--bottom)} + body.has-rail .toast{left:50%;bottom:calc(var(--bottom) + 12px)} + body.drawer-open .drawer{bottom:var(--bottom)} + .ask-wrap{bottom:var(--bottom)} + .top{padding:0 var(--s-4)} + .page-title h1{font-size:17px} + .pill{min-width:0} + .hero-row{grid-template-columns:1fr} + .toggle-big{min-height:88px} + .gl-main{grid-template-columns:44px 1fr;gap:var(--s-3);padding:12px} + .nums{grid-column:1 / -1;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s-2) var(--s-3);justify-items:start} + .acts{grid-column:1 / -1;grid-row:auto;justify-self:end} + .gpu-line .name{font-size:var(--t-base)} + .num{align-items:flex-start;text-align:left} + .acts{gap:var(--s-2)} + .gl-tune,.gl-details{padding-left:12px;padding-right:12px} + .gl-details .ctl{grid-template-columns:1fr;gap:6px} + .gl-details .ctl .help:first-of-type{grid-column:1} + .ids{justify-self:start} + .kv>div{grid-template-columns:96px 1fr;gap:4px var(--s-3)} + .kv .m{grid-column:1 / -1;white-space:normal} + .goal-row{align-items:flex-start;flex-direction:column} + .seg{width:100%} + .seg-b{flex:1;padding:6px 8px} + .indent{margin-left:0} + .disclose-right .dim{display:none} + .lead-row{flex-direction:column} + .step{padding:32px 0} + .money-row .v{font-size:var(--t-num)} + .drawer-head{padding-left:var(--s-4);padding-right:var(--s-4)} + .log,.log-ruler .t0,.log-ruler .t1{padding-left:var(--s-4);padding-right:var(--s-4)} +} +/* short windows (the 600 px floor): tighter paddings, a smaller canvas, so the drawer always has room */ @media (max-height:700px){ - :root{--card-pad:18px;--tile-pad:14px 16px;--gap:var(--s-4)} + :root{--card-pad:18px;--gap:var(--s-3)} #screen-dashboard{padding:16px 0 20px} - #dag{height:140px} + #dag{height:90px} .hero{gap:var(--s-3);padding:var(--s-5) 0 var(--s-6)} - .coin-wrap{width:104px;height:104px} - .coin{width:104px;height:104px} + .mark-wrap{width:88px;height:88px} h1{font-size:40px} - .lead{font-size:var(--t-xl)} + .lead{font-size:var(--t-lg)} .step{padding:32px 0 32px} - .feed{max-height:200px} .drawer-head{padding-bottom:6px} .toggle-big{min-height:96px} } diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 57c14596..01549746 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -1,19 +1,12 @@ -/* Igneum Miner dashboard. Talks to the engine on the same origin: api/state every second, api/log while the - drawer is open. Screens: welcome, cards, address (plus the one-time key sheet), then the dashboard with six pages behind a rail - (Mine, Prove, Rewards, Node, Updates, Settings; miner-ui-2). ?screen= forces a screen, ?page= a page (screenshots). Everything is relative to the token path the page was opened on. */ +/* Igneum Miner dashboard (miner-ui-3). Talks to the engine on the same origin: api/state every second, api/log while + the drawer is open. Screens: welcome, cards, address (plus the one-time key sheet), then the dashboard with four + pages behind a rail (Mine, Earnings, Prove, Settings). ?screen= forces a screen, ?page= a page, and + ?update=, ?job=, ?tune= show sample states (screenshots). Everything is relative to the token path the page was + opened on. The pure blocks (Notices, UpdateCard, View, TuneLine) run under node for the tests. */ -/* ---------- notices: what the one strip under the header says (pure; notices.test.mjs loads this block) ---------- - The state (src/state.rs) becomes at most one notice at a time, the most important first. Each notice has a key; - closing it hides that key until the state moves on (a new status, version or job). Levels, lowest number first: - 0 update installing, urgent or failed 1 job failed 2 clock 3 job running - 4 update available, downloading, ready, waiting for permission, manual 5 job done 6 updated - Rules: a done job goes after 5 minutes; a failed job stays until closed; "updated" goes 60 s after the new - version started; the clock notice is for the setup screens (the node card carries it on the dashboard); the job - notice is for the dashboard. A card that appeared or went (hot-plug, src/hotplug.rs) is a level 5 notice for - 5 minutes on every screen: "New card: , mining", "New card: , not usable (Code 43)", "Card removed". */ var Notices = (function () { 'use strict'; - var LEVEL = { 'update-installing': 0, 'update-urgent': 0, 'update-failed': 0, 'job-failed': 1, clock: 2, 'job-running': 3, 'update-available': 4, 'job-done': 5, 'card-added': 5, 'card-removed': 5, 'update-installed': 6 }; + var LEVEL = { 'update-installing': 0, 'update-urgent': 0, 'update-failed': 0, 'job-failed': 1, clock: 2, 'job-running': 3, tuning: 3, 'update-available': 4, 'job-done': 5, 'card-added': 5, 'card-removed': 5, 'update-installed': 6 }; var JOB_DONE_S = 300, INSTALLED_S = 60, CARD_S = 300; function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; } function firstLine(t, max) { t = String(t || '').split('\n')[0].trim(); max = max || 160; return t.length > max ? t.slice(0, max - 1).trim() + '\u2026' : t; } @@ -120,8 +113,19 @@ var Notices = (function () { if (where.dashboard) { var j = jobNotice(s.jobs, s.now || 0); if (j) out.push(j); } else { var c = clockNotice(s.clock); if (c) out.push(c); } if (s.mining && s.mining.cards) out = out.concat(cardNotices(s.mining.cards, s.now || 0)); + var tn = tuneNotice(s.mining && s.mining.cards); + if (tn) out.push(tn); return out; } + // the one line while a card is being tuned: which card, which plan, the step, when mining is back + function tuneNotice(cards) { + var l = (cards || []).filter(function (c) { return c.state === 'tuning'; }); + if (!l.length) return null; + var c = l[0], plan = c.tune_plan === 'confirm' ? 'confirming the fleet prior' : c.tune_plan === 'baseline' ? 'measuring as it runs' : 'the full tune'; + var step = c.tune_steps > 0 ? ', step ' + (c.tune_step || 1) + ' of ' + c.tune_steps : ''; + var eta = c.tune_eta_s > 0 ? ' Mining again in ' + (c.tune_eta_s < 60 ? 'under a minute' : 'about ' + Math.round(c.tune_eta_s / 60) + ' min') + '.' : ''; + return notice('tuning', 'tuning:' + c.key, 'Tuning ' + c.name + ': ' + plan + step + '.' + eta, { tone: 'info', title: 'Ember Tune' }); + } // the lowest level wins; a dismissed key is skipped, so the next one shows in its place function pick(list, dismissed) { var best = null; @@ -132,11 +136,11 @@ var Notices = (function () { } return best; } - return { LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, CARD_S: CARD_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, cardNotices: cardNotices, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot, kindWord: kindWord }; + return { tuneNotice: tuneNotice, LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, CARD_S: CARD_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, cardNotices: cardNotices, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot, kindWord: kindWord }; })(); /* ---------- the update card (pure; update-card.test.mjs loads this block) ---------- - A centred card over the window for one update: the mark with a progress ring, "Igneum Ember 0.3.7", one line + A centred card over the window for one update: the mark with a progress ring, "Igneum Miner 0.3.14", one line (is available, is downloading, is ready to install, Installing, did not install), up to three lines of release notes with the rest behind "What changed", the size, Install now and Later. The same card, the same rules and the same words sit in the wallet (app/igneum-wallet/ui/update-card.js). @@ -149,7 +153,7 @@ var Notices = (function () { installing and failed show only on an open card (Install now was pressed here); the strip carries the rest */ var UpdateCard = (function () { 'use strict'; - var NAME = 'Igneum Ember', LINE_MAX = 70, LINES = 3, FIRST_S = 60; + var NAME = 'Igneum Miner', LINE_MAX = 70, LINES = 3, FIRST_S = 60; var INSTALL = { act: 'install', label: 'Install now', primary: true }, LATER = { act: 'later', label: 'Later' }; var RETRY = { act: 'retry', label: 'Try again', primary: true }, OPEN = { act: 'open', label: 'Open the download', primary: true }; function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; } @@ -238,24 +242,29 @@ var UpdateCard = (function () { return { NAME: NAME, LINE_MAX: LINE_MAX, LINES: LINES, FIRST_S: FIRST_S, sentences: sentences, splitNotes: splitNotes, size: size, model: model, blocked: blocked, decide: decide }; })(); /* ---------- the pages' words (pure; view.test.mjs loads this block) ---------- - What each section says for a given state: the GPU row on Mine, the big start/stop button, the Node page's plain - lines, the next consensus switch, the Prove page's words, the dev-fee line. No DOM, no time of day: the caller - passes `now`. The sections and their order: Mine, Prove, Rewards, Node, Updates, Settings. */ + What each section says for a given state: the card row (every state word, the numbers, the money, the tune line), + the big start/stop button, the node line, the prove tier sentence per card, the goal's consequence, the dev-fee + line, the confirmations' words. No DOM, no time of day: the caller passes `now`. The sections (miner-ui-3): Mine, + Earnings, Prove, Settings. */ var View = (function () { 'use strict'; var PAGES = [ - { id: 'mine', title: 'Mine', sub: 'your GPUs, the hash rate, the blocks you found' }, - { id: 'prove', title: 'Prove', sub: 'turn blocks into proofs and get paid for each one' }, - { id: 'rewards', title: 'Rewards', sub: 'where the blocks pay, and the key that controls it' }, - { id: 'node', title: 'Node', sub: 'the chain on this machine' }, - { id: 'updates', title: 'Updates', sub: 'the app version and the jobs Igneum sent' }, - { id: 'settings', title: 'Settings', sub: 'power, start at login, remote jobs, the dev fee' } + { id: 'mine', title: 'Mine', sub: 'your cards, the rate, what it costs' }, + { id: 'earnings', title: 'Earnings', sub: 'what it made, where it goes' }, + { id: 'prove', title: 'Prove', sub: 'turn blocks into proofs, paid in IGN' }, + { id: 'settings', title: 'Settings', sub: 'tuning, this machine, updates, logs' } ]; + var DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + var SHARD_GB = 20.4, PROVE_GB = 24; function page(id) { for (var i = 0; i < PAGES.length; i++) if (PAGES[i].id === id) return PAGES[i]; return PAGES[0]; } function withCommas(n) { return String(Math.round(n || 0)).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function compact(n) { if (n === null || n === undefined || isNaN(n)) return '0'; var a = Math.abs(n); if (a >= 1e12) return (n / 1e12).toFixed(2) + 'T'; if (a >= 1e9) return (n / 1e9).toFixed(2) + 'G'; if (a >= 1e6) return (n / 1e6).toFixed(2) + 'M'; if (a >= 1e4) return (n / 1e3).toFixed(1) + 'k'; return String(Math.round(n)); } function rel(s) { s = Math.max(0, Math.round(s)); if (s < 60) return s + ' s ago'; var m = Math.floor(s / 60); if (m < 60) return m + ' min ago'; var h = Math.floor(m / 60); if (h < 24) return h + ' h ' + (m % 60) + ' min ago'; return Math.floor(h / 24) + ' d ago'; } + // a coarser "ago" for a line that is read, not measured: "just now", "12 min ago", "2 h ago", "3 d ago" + function ago(s) { s = Math.max(0, Math.round(s)); if (s < 60) return 'just now'; if (s < 3600) return Math.floor(s / 60) + ' min ago'; if (s < 86400) return Math.floor(s / 3600) + ' h ago'; return Math.floor(s / 86400) + ' d ago'; } function shortHex(h, head, tail) { h = String(h || ''); head = head || 8; tail = tail || 6; return h.length > head + tail + 2 ? h.slice(0, head) + '…' + h.slice(-tail) : h; } + function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; } var kindWord = Notices.kindWord; // hot-plug (src/hotplug.rs): a removed card's row hides after five minutes (gone); a faulty one has no switch // The list is ordered by performance (the project lead, 6 October 2026): usable cards first, then by the measured rate since the @@ -282,9 +291,11 @@ var View = (function () { function cardRow(cd) { var removed = cd.removed_at > 0, unusable = !!cd.problem; var on = !!cd.enabled && !removed && !unusable, st = removed ? 'removed' : unusable ? 'unusable' : (cd.state || 'off'); - var word = removed ? 'removed' : unusable ? ('not usable (' + cd.problem + ')') : !on ? 'off' : st === 'mining' ? 'mining' : st === 'restarting' ? 'restart in ' + (cd.restart_in_s || 0) + ' s' : st === 'waiting' ? 'waiting for the node' : st === 'faulted' ? 'stopped after repeated faults' : st === 'failed' ? 'failed' : st === 'starting' ? 'starting' : st === 'ready' ? 'ready' : st; - var tone = removed ? 'off' : unusable ? 'bad' : !on ? 'off' : st === 'mining' ? 'on' : (st === 'failed' || st === 'faulted' || st === 'restarting') ? 'bad' : ''; - var hash = on && st === 'mining' ? (cd.hash_now >= 100 ? cd.hash_now.toFixed(0) : (cd.hash_now || 0).toFixed(1)) : ''; + // the rule (docs/plans/ember-tune.md, 6 October 2026): a card never shows 0 MH/s without a reason word + var tuning = st === 'tuning', held = st === 'held'; + var word = removed ? 'removed' : unusable ? ('not usable (' + cd.problem + ')') : !on ? 'off' : st === 'mining' ? 'mining' : tuning ? tuneWord(cd) : held ? (cd.message || 'held for a remote job') : st === 'restarting' ? 'restart in ' + (cd.restart_in_s || 0) + ' s' : st === 'waiting' ? 'waiting for the node' : st === 'faulted' ? 'stopped after repeated faults' : st === 'failed' ? 'failed' : st === 'starting' ? 'starting' : st === 'ready' ? 'ready' : st; + var tone = removed ? 'off' : unusable ? 'bad' : !on ? 'off' : (st === 'mining' || tuning) ? 'on' : (st === 'failed' || st === 'faulted' || st === 'restarting') ? 'bad' : ''; + var hash = on && (st === 'mining' || tuning) ? (cd.hash_now >= 100 ? cd.hash_now.toFixed(0) : (cd.hash_now || 0).toFixed(1)) : ''; var temp = cd.temp_gpu > 0 ? Math.round(cd.temp_gpu) + ' °C' : ''; var tempTone = cd.temp_mem > 95 || cd.temp_gpu > 92 ? 'hot' : cd.temp_mem > 90 || cd.temp_gpu > 86 ? 'warm' : ''; var power = cd.power_w > 0 ? Math.round(cd.power_w) + ' W' : ''; @@ -297,18 +308,134 @@ var View = (function () { return { key: cd.key, name: cd.name, vendor: cd.vendor, kind: cd.kind, kindWord: kindWord(cd.kind), integrated: cd.kind === 'integrated', on: on, removed: removed, unusable: unusable, canToggle: cd.kind !== 'unknown' && !removed && !unusable, word: word, tone: tone, hash: hash, temp: temp, tempTone: tempTone, power: power, cap: cap, sub: sub, meta: meta.join(' · '), title: cardTitle(cd) }; } + // "tuning: step 7 of 9 · measuring 220 W" (the live draw; the plan's words when there is no draw yet) + function tuneWord(cd) { + var step = cd.tune_steps > 0 ? 'step ' + (cd.tune_step || 1) + ' of ' + cd.tune_steps : ''; + var w = cd.power_w > 0 ? 'measuring ' + Math.round(cd.power_w) + ' W' : ''; + var rest = [step, w].filter(Boolean).join(' · '); + return rest ? 'tuning: ' + rest : (cd.sweep_note || 'tuning'); + } + // "about 12 min" / "under a minute" from a tune's seconds left + function tuneEta(s) { s = Math.max(0, Math.round(s || 0)); return s < 60 ? 'under a minute' : 'about ' + Math.round(s / 60) + ' min'; } + // the card a tune is running on, or null + function tuningCard(cards) { var l = (cards || []).filter(function (c) { return c.state === 'tuning' || c.sweep_state === 'running'; }); return l.length ? l[0] : null; } + + // ---- money (miner-ui-3): £ a day from watts at pence per kWh; null when either is unknown ---- + function poundsPerDay(watts, pence) { return watts > 0 && pence > 0 ? watts * 24 / 1000 * pence / 100 : null; } + function money(v) { if (v === null || v === undefined || isNaN(v)) return ''; return '£' + (v >= 100 ? String(Math.round(v)) : v.toFixed(2)); } + // MH per watt: the engine's live figure, else rate over draw + function effOf(cd) { if (cd.eff_mhw > 0) return cd.eff_mhw; return cd.power_w > 0 && cd.hash_now > 0 ? cd.hash_now / cd.power_w : 0; } + function effText(e) { return e > 0 ? (e >= 10 ? e.toFixed(1) : e.toFixed(2)) + ' MH/W' : ''; } + // the watts drawn now by the present cards + function wattsTotal(cards) { return (cards || []).filter(present).reduce(function (a, c) { return a + (c.power_w > 0 ? c.power_w : 0); }, 0); } + // why a row has no power or temperature cell: one sentence, once, instead of three n/a + function noReading(cd) { + if (cd.power_w > 0 || cd.temp_gpu > 0) return ''; + if (cd.vendor === 'apple') return 'No power or temperature reading on Apple silicon'; + if (cd.kind === 'integrated') return 'No power or temperature reading on an integrated GPU'; + return ''; + } + // the row's cells: rate, watts with MH/W, £ a day, temperature; a cell with no number is left out, never n/a + function cells(cd, pence) { + var r = cardRow(cd), out = {}; + out.hash = r.hash; out.power = r.power; out.temp = r.temp; out.tempTone = r.tempTone; + out.eff = effText(effOf(cd)); + var ppd = poundsPerDay(cd.power_w, pence); + out.money = ppd === null ? '' : money(ppd); + out.needsPrice = cd.power_w > 0 && !(pence > 0); + out.noReading = noReading(cd); + return out; + } + + // ---- the tune line under a row (miner-ui-3 section 4): cd, s = state, now ---- + function nextCheck(at, period, now) { + if (!(at > 0) || !now) return ''; + period = period > 0 ? period : 604800; + var next = at + period, d = next - now; + if (d <= 0) return 'next check due now'; + if (d < 86400) return 'next check within a day'; + var dt = new Date(next * 1000); + if (d < 6 * 86400) return 'next check ' + DAYS[dt.getDay()]; + return 'next check ' + dt.getDate() + ' ' + MONTHS[dt.getMonth()]; + } + function tunePoint(cd) { + var pct = cd.sweep_pct || cd.power_pct || 0; + if (cd.tune_clock_mhz > 0) return cd.tune_clock_mhz + ' MHz at ' + pct + '%'; + return pct ? pct + '%, clock unlocked' : ''; + } + function reasonOf(note) { return cap(String(note || '').replace(/^measure only /, 'measured only ')); } + function tunable(cd) { return (cd.vendor === 'nvidia' || cd.vendor === 'amd' || cd.vendor === 'apple' || !!cd.sweep_supported) && present(cd) && cd.kind !== 'integrated'; } + // {kind: none|paused|running|measured|tuned|stopped|idle, text, note, button: ''|'tune'|'stop', progress: -1 or 0..1} + function tuneWords(cd, s, now) { + cd = cd || {}; var st = (s && s.settings) || {}; + if (!tunable(cd)) return { kind: 'none', text: '', note: '', button: '', progress: -1 }; + if (st.tuning_off) return { kind: 'paused', text: st.tuning_note || 'Tuning paused fleet-wide by the signed manifest', note: '', button: '', progress: -1 }; + if (cd.state === 'tuning' || cd.sweep_state === 'running') { + var step = cd.tune_steps > 0 ? 'step ' + (cd.tune_step || 1) + ' of ' + cd.tune_steps : (cd.sweep_note ? cd.sweep_note.replace(/^tuning:\s*/, '') : 'running'); + var eta = cd.tune_eta_s > 0 ? ' · ' + tuneEta(cd.tune_eta_s) + ' left' : ''; + var prog = cd.tune_steps > 0 ? Math.min(1, Math.max(0, ((cd.tune_step || 1) - 1) / cd.tune_steps)) : 0; + return { kind: 'running', text: 'Tuning: ' + step + eta, note: '', button: 'stop', progress: prog }; + } + var when = cd.sweep_at > 0 && now ? ago(now - cd.sweep_at) : ''; + var nc = nextCheck(cd.sweep_at, st.tune_period_s, now); + if (cd.tune_line) { + var nums = String(cd.tune_line).replace(/^Tuned:\s*/, ''); + if (cd.tune_source === 'baseline' || !cd.tune_control) return { kind: 'measured', text: 'Measured ' + nums + ' as it runs' + (when ? ', ' + when : ''), note: reasonOf(cd.sweep_note), button: 'tune', progress: -1 }; + var src = cd.tune_source === 'confirm' ? ' from the fleet prior, confirmed' : ''; + var pin = cd.pinned ? 'Your cap stays pinned at ' + (cd.power_pct || 0) + '% · ' : ''; + var pt = tunePoint(cd); + var stopped = cd.sweep_note && /^tuning stopped/i.test(cd.sweep_note) ? ' · ' + cap(cd.sweep_note) : ''; + return { kind: 'tuned', text: pin + 'Tuned ' + (when || 'before') + src + (pt ? ' · ' + pt : '') + (nc ? ' · ' + nc : '') + stopped, note: nums, button: 'tune', progress: -1 }; + } + if (cd.sweep_note && /^tuning stopped/i.test(cd.sweep_note)) return { kind: 'stopped', text: cap(cd.sweep_note), note: '', button: 'tune', progress: -1 }; + var why = !cd.tune_control && cd.sweep_note ? reasonOf(cd.sweep_note) : ''; + var text = st.sweep === false ? 'Not tuned yet: Ember Tune is off in Settings' : 'Not tuned yet: starts after 2 min of steady mining'; + return { kind: 'idle', text: text, note: why, button: 'tune', progress: -1 }; + } + // the Tuning card's one line: what runs, what ran last, what comes next + function schedule(cards, s, now) { + var st = (s && s.settings) || {}; + if (st.tuning_off) return st.tuning_note || 'Tuning paused fleet-wide by the signed manifest.'; + var list = shownCards(cards).filter(tunable), tc = tuningCard(list); + if (tc) { var step = tc.tune_steps > 0 ? ', step ' + (tc.tune_step || 1) + ' of ' + tc.tune_steps : ''; return 'Tuning now: ' + tc.name + step + (tc.tune_eta_s > 0 ? ', mining again in ' + tuneEta(tc.tune_eta_s) : '') + '.'; } + var last = null; + list.forEach(function (c) { if (c.sweep_at > 0 && (!last || c.sweep_at > last.sweep_at)) last = c; }); + if (last) return 'Last tune ' + ago(now - last.sweep_at) + ' (' + last.name + ') · ' + nextCheck(last.sweep_at, st.tune_period_s, now) + '.'; + if (!list.length) return 'No card here can be tuned.'; + return st.sweep === false ? 'Ember Tune is off. Tune and Tune all still work by hand.' : 'Not tuned yet: the first tune starts 2 min into steady mining.'; + } + // the goal control's consequence: goal = efficiency|balanced|rate, cards, pence + var GOALS = { efficiency: 'the most hashes per watt', balanced: 'hashes per watt within 1% of the top rate', rate: 'the top rate, whatever the watts' }; + function goalWords(goal, cards, pence) { + goal = GOALS[goal] ? goal : 'balanced'; + var list = (cards || []).filter(present), w = 0; + list.forEach(function (c) { + if (goal === 'efficiency') w += c.sweep_watts > 0 ? c.sweep_watts : (c.power_w > 0 ? c.power_w * 0.9 : 0); + else if (goal === 'rate') w += c.power_default_w > 0 ? c.power_default_w : (c.power_w || 0); + else w += c.power_w > 0 ? c.power_w : 0; + }); + var ppd = poundsPerDay(w, pence); + return { goal: goal, label: GOALS[goal], watts: Math.round(w), consequence: ppd === null ? (w > 0 ? 'about ' + Math.round(w) + ' W' : '') : 'about ' + money(ppd) + ' a day at ' + Math.round(w) + ' W' }; + } + // the big button on Mine: m = state.mining, n = state.node, s = { quitting } function toggle(m, n, s) { var cards = shownCards(m.cards).filter(present); var on = cards.filter(function (c) { return c.enabled; }).length; var mining = cards.filter(function (c) { return c.state === 'mining'; }).length; if (s && s.quitting) return { label: 'Stopping', sub: 'miners first, then the node', cls: 'stop', disabled: true, act: '' }; + var tc = tuningCard(cards); + if (tc) return { label: 'Tuning', sub: 'mining again in ' + tuneEta(tc.tune_eta_s) + ' · ' + tc.name, cls: 'stop', disabled: true, act: '' }; + var heldCards = cards.filter(function (c) { return c.state === 'held'; }).length; + if (heldCards && !mining) return { label: 'Held', sub: 'a remote job holds the cards; mining restarts when it ends', cls: 'stop', disabled: true, act: '' }; if (!on) return { label: 'Start mining', sub: cards.length ? 'switch a GPU on below first' : 'no GPU this app can drive', cls: '', disabled: true, act: '' }; if (m.paused) return { label: 'Start mining', sub: 'paused · the node keeps running', cls: '', disabled: false, act: 'resume' }; if (m.state === 'mining') return { label: 'Stop mining', sub: 'mining on ' + mining + ' of ' + on + ' card' + (on === 1 ? '' : 's'), cls: 'stop', disabled: false, act: 'pause' }; if (m.state === 'waiting') return { label: 'Stop mining', sub: n && n.synced ? 'worker starting' : 'waiting for the node to sync', cls: 'stop', disabled: false, act: 'pause' }; return { label: 'Stop mining', sub: m.state || 'starting', cls: 'stop', disabled: false, act: 'pause' }; } + // the row's state word while the miner is paused: the card is on, the miner is not + function rowWord(r, m) { return r.on && m && m.paused && (r.word === 'off' || r.word === 'ready' || r.word === 'waiting for the node') ? 'paused' : r.word; } // the Node page's plain words: n = state.node, clock = state.clock, eta = the sync ETA sentence (may be '') function nodeWords(n, clock, eta) { @@ -327,6 +454,13 @@ var View = (function () { } return { word: n.state || 'starting', line: n.message || '', tone: '' }; } + // the one node line on Mine: "Node synced · 6 peers · 109,859 blocks" + function nodeLine(n, clock, eta) { + var w = nodeWords(n, clock, eta), parts = ['Node ' + w.word]; + if (n.peers > 0) parts.push(n.peers + ' peer' + (n.peers === 1 ? '' : 's')); + if (n.blocks > 0) parts.push(withCommas(n.blocks) + ' blocks'); + return { text: parts.join(' · '), sub: w.line, tone: w.tone }; + } function skewWord(sk) { sk = Math.round(Math.abs(sk || 0)); return sk < 120 ? sk + ' s' : sk < 7200 ? Math.floor(sk / 60) + ' min ' + (sk % 60) + ' s' : Math.floor(sk / 3600) + ' h ' + Math.floor(sk % 3600 / 60) + ' min'; } function peersLine(n) { return n.peers === 0 ? 'none yet: looking for the seed node' : n.peers === 1 ? 'one other node this one talks to' : 'other nodes this one talks to'; } function heightLine(n) { return n.headers > n.blocks + 2 ? 'of ' + withCommas(n.headers) + ' headers seen' : 'blocks this node holds'; } @@ -346,7 +480,7 @@ var View = (function () { // the Prove page: pv = state.proving, enabled = state.settings.prove, synced = state.node.synced function proveWords(pv, enabled, synced) { pv = pv || {}; - if (!enabled) return { word: 'off', sub: 'not proving', tone: 'off', note: 'Off. Switch it on and this machine proves the shards the chain assigns to its keys. On a Mac the CPU prover is slow; a CUDA card is quick.' }; + if (!enabled) return { word: 'off', sub: 'not proving', tone: 'off', note: 'Off. Switch it on and this machine proves the pieces the chain assigns to its keys.' }; if (pv.status === 'setup' || (pv.enabled && !pv.available && pv.setup_hint)) return { word: 'needs setup', sub: 'one-time install', tone: 'bad', note: pv.setup_hint || pv.message || 'The prover is not installed yet.' }; if (!synced || pv.status === 'waiting') return { word: 'waiting', sub: 'for the node to sync', tone: '', note: pv.message || 'Proving starts once the node has every block.' }; if (pv.status === 'proving') return { word: 'proving', sub: pv.current || 'a shard', tone: 'on', note: pv.message || 'A proof takes minutes on a CPU and seconds on a CUDA card.' }; @@ -360,6 +494,20 @@ var View = (function () { if (vm === 'command' && pv.pool_entries) word += ' · pool ' + pv.pool_verified + '/' + pv.pool_entries + (pv.pool_failed ? ', ' + pv.pool_failed + ' rejected' : ''); return { word: word, tone: vm === 'command' ? 'ok' : (vm === 'off' || vm === 'trust') ? 'warn' : 'dim', needsSetup: vm === 'off' && /WSL2 prover is not installed/.test((pv && pv.verifier_reason) || '') }; } + // the tier sentence per card: can this card prove, and how (the 24 GB rule and the 20.4 GB shard are the app's own + // words since miner-ui-2; the engine's proving.available wins when it disagrees) + function proveTier(cd) { + var gb = cd.vram_mb ? Math.round(cd.vram_mb / 1024) : 0; + if (cd.vendor === 'apple') return cd.name + ': proves on the CPU, slowly.'; + if (cd.vendor === 'nvidia') return gb >= PROVE_GB ? cd.name + ': proves while it mines (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).' : cd.name + ': cannot prove a full shard (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).'; + return cd.name + ': cannot prove yet (the prover is CUDA only).'; + } + // the counts in one sentence: "idle · nothing assigned · 0 assigned · 0 proven · 0 paid · 0.0000 IGN" + function proveLine(pv, enabled, synced) { + pv = pv || {}; + var w = proveWords(pv, enabled, synced), ign = pv.paid_wei && Number(pv.paid_wei) ? (Number(pv.paid_wei) / 1e18).toFixed(4) : '0.0000'; + return cap(w.word) + (w.sub ? ' · ' + w.sub : '') + ' · ' + (pv.assigned || 0) + ' assigned · ' + (pv.submitted || 0) + ' proven · ' + (pv.paid || 0) + ' paid · ' + ign + ' IGN'; + } // the miner's own dev-fee line, next to the rewards address function devFeeText(s) { @@ -370,26 +518,37 @@ var View = (function () { return 'Dev fee ' + (d.percent || 1) + '% (1 block in 100)' + addr + (d.address ? '' : '; the miner names the address when it starts'); } function devFeeLine(s) { - return s.settings.dev_fee ? 'One block in 100 pays the miner software’s dev fee' + (s.mining.fee_total ? ' (' + withCommas(s.mining.fee_total) + ' so far)' : '') + '. Settings turns it off.' : 'The dev fee is off. Every block pays this address.'; + return s.settings.dev_fee ? 'One block in 100 pays the miner software’s dev fee' + (s.mining.fee_total ? ' (' + withCommas(s.mining.fee_total) + ' so far)' : '') + '. The switch above turns it off.' : 'The dev fee is off. Every block pays this address.'; } - // the remote-jobs line on Updates: j = state.jobs, now = state.now + // the remote-jobs line on Settings: j = state.jobs, now = state.now function jobsNote(j, now, jobTitle) { var parts = []; if (j.account) parts.push(j.account); - if (!j.allowed) parts.push('Off: nothing runs here until Settings allows remote jobs.'); + if (!j.allowed) parts.push('Off: nothing runs here until the switch above allows remote jobs.'); else if (!j.url_set) parts.push('No jobs address in this build.'); else if (j.error) parts.push(j.error + '.'); else if (j.active) parts.push('Running ' + jobTitle(j) + ' (' + j.id + ').'); - else parts.push('Nothing running' + (j.checked_at ? '; checked ' + rel((now || 0) - j.checked_at) : '') + (j.queued ? '; ' + j.queued + ' queued' : '') + '.'); + else parts.push('Nothing running' + (j.checked_at ? '; checked ' + rel((now || 0) - j.checked_at) : '') + (j.queued ? '; ' + j.queued + ' queued' : '') + ((j.history || []).length ? '; ' + j.history.length + ' job' + (j.history.length === 1 ? '' : 's') + ' ran here' : '') + '.'); return parts.join(' '); } - return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, toggle: toggle, nodeWords: nodeWords, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote }; + // an activity line for a user: a JSON tail goes to the log, a long line is cut at a word + function eventLine(text, max) { + text = String(text || ''); max = max || 140; + var j = text.indexOf('{'); if (j > 0) text = text.slice(0, j).replace(/[:\s]+$/, '') + ' (details in the log)'; + if (text.length > max) { var cut = text.lastIndexOf(' ', max - 2); text = text.slice(0, cut > 40 ? cut : max - 2) + '…'; } + return text; + } + // the confirmations' words (in place, never a dialog) + function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? The miner restarts.'; } + function trustAsk(on) { return on ? 'Include proof records the node never checked? Devnet only; the node restarts.' : 'Verify proof records again? The node restarts.'; } + return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, goalWords: goalWords, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, addressAsk: addressAsk, trustAsk: trustAsk }; })(); /* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ---------- One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned ("Tuned: 122.3 MH/s at 290 W (0.422 MH/W)" plus the point and when), measure only (Apple, NVIDIA without Power - control: the measured line and why nothing is set), stopped (the reason), or not run yet. */ + control: the measured line and why nothing is set), stopped (the reason), or not run yet. The Settings-page block + of miner-ui-2; miner-ui-3 reads View.tuneWords for the row and keeps this for the details and ember-tune's test. */ var TuneLine = (function () { 'use strict'; function point(cd) { @@ -412,9 +571,11 @@ var TuneLine = (function () { return { kind: 'idle', text: cd.sweep_note || 'tuning: not run yet (starts after 120 s of steady mining)' }; } function rel(s) { s = Math.max(0, Math.floor(s)); return s < 60 ? s + ' s' : s < 3600 ? Math.floor(s / 60) + ' min' : s < 86400 ? Math.floor(s / 3600) + ' h' : Math.floor(s / 86400) + ' d'; } - return { model: model, point: point, rel: rel }; + // Ember 2: £ a day for a draw at a price in pence per kWh (the same formula as View.poundsPerDay and ember::pounds_per_day) + function pounds(watts, pence) { return View.money(View.poundsPerDay(watts, pence)); } + return { model: model, point: point, rel: rel, pounds: pounds }; })(); -if (typeof module === 'object' && module && module.exports) { module.exports = Notices; module.exports.UpdateCard = UpdateCard; module.exports.View = View; module.exports.TuneLine = TuneLine; } +if (typeof module === 'object' && module && module.exports) { module.exports = Notices; module.exports.UpdateCard = UpdateCard; module.exports.View = View; module.exports.TuneLine = TuneLine; module.exports.tuneNotice = Notices.tuneNotice; } if (typeof document !== 'undefined') (function () { 'use strict'; @@ -424,7 +585,7 @@ if (typeof document !== 'undefined') (function () { var state = null, phase = 'welcome', page = 'mine', forced = null, cardsSeen = false, keyShownThisRun = false; var params = new URLSearchParams(location.search); if (params.get('screen')) forced = params.get('screen'); - if (params.get('page')) page = params.get('page'); // ?page=mine|prove|rewards|node|updates|settings (screenshots) + if (params.get('page')) page = params.get('page'); // ?page=mine|earnings|prove|settings (screenshots) var forcedUpdate = params.get('update'); // a sample update state for screenshots: available|downloading|ready|waiting|applying|urgent|manual|error|updated var forcedJob = params.get('job'); // a sample remote job for screenshots: running|done|failed if (params.get('host') === 'mac') document.body.classList.add('mac'); @@ -451,11 +612,14 @@ if (typeof document !== 'undefined') (function () { return fetch(path, opt).then(function (r) { return r.json(); }); } function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } - var compact = View.compact, withCommas = View.withCommas, rel = View.rel, kindWord = View.kindWord, shownCards = View.shownCards, cardTitle = View.cardTitle; + var compact = View.compact, withCommas = View.withCommas, rel = View.rel, kindWord = View.kindWord, shownCards = View.shownCards, cardTitle = View.cardTitle, money = View.money; function hms(s) { s = Math.max(0, Math.round(s)); var h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60), x = s % 60; return (h ? h + ':' : '') + (h ? String(m).padStart(2, '0') : m) + ':' + String(x).padStart(2, '0'); } function uptime(s) { var h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60); return h ? h + ' h ' + m + ' min' : m + ' min'; } function clock(t) { var d = new Date(t * 1000); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') + ':' + String(d.getSeconds()).padStart(2, '0'); } function setText(id, text) { var el = $(id); if (el && el.textContent !== text) el.textContent = text; } + function setHidden(id, hidden) { var el = $(id); if (el && el.hidden !== !!hidden) el.hidden = !!hidden; } + function store(key, val) { try { if (val === null) localStorage.removeItem(key); else localStorage.setItem(key, String(val)); } catch (e) { } } + function stored(key) { try { return localStorage.getItem(key); } catch (e) { return null; } } var toastT = null; function toast(text) { var t = $('toast'); t.textContent = text; t.hidden = false; clearTimeout(toastT); toastT = setTimeout(function () { t.hidden = true; }, 2200); } function copyText(text, msg) { @@ -470,6 +634,19 @@ if (typeof document !== 'undefined') (function () { if (!t || /not (set|read|printed)/.test(t)) { toast('Nothing to copy yet'); return; } copyText(t); }); + // the electricity price: the engine's setting (ember-tune), else what this page stored; pence per kWh + function priceNow() { var p = state && state.settings && state.settings.power_price_pence; if (p > 0) return p; var s = parseFloat(stored('igneum.price') || ''); return s > 0 ? s : 0; } + function goalNow() { var g = state && state.settings && state.settings.tune_goal; return View.GOALS[g] ? g : (View.GOALS[stored('igneum.goal') || ''] ? stored('igneum.goal') : 'balanced'); } + + // ---------- appearance: system, light or dark (the page's own storage) ---------- + function applyTheme(t) { + t = t === 'light' || t === 'dark' ? t : 'system'; + if (t === 'system') document.documentElement.removeAttribute('data-theme'); else document.documentElement.setAttribute('data-theme', t); + document.querySelectorAll('#theme-seg .seg-b').forEach(function (b) { var on = b.dataset.theme === t; b.classList.toggle('on', on); b.setAttribute('aria-checked', on ? 'true' : 'false'); }); + store('igneum.theme', t === 'system' ? null : t); + } + applyTheme(stored('igneum.theme') || 'system'); + $('theme-seg').addEventListener('click', function (e) { var b = e.target.closest('.seg-b'); if (b) applyTheme(b.dataset.theme); }); // ---------- screens (setup) and pages (the dashboard) ---------- function show(p) { @@ -497,14 +674,14 @@ if (typeof document !== 'undefined') (function () { }); setText('page-title-text', meta.title); setText('page-sub', meta.sub); $('main').scrollTop = 0; - if (state) { renderPage(state); if (page === 'settings') fillSettings(); if (page === 'mine') size(); } + if (state) { renderPage(state); if (page === 'mine') size(); } } $('rail-nav').addEventListener('click', function (e) { var b = e.target.closest('.nav[data-page]'); if (b) showPage(b.dataset.page); }); // the rail answers the arrow keys and Home/End, so the sections are reachable without a mouse $('rail-nav').addEventListener('keydown', function (e) { var items = Array.prototype.slice.call(document.querySelectorAll('#rail-nav .nav')), i = items.indexOf(document.activeElement); if (i < 0) return; - var j = e.key === 'ArrowDown' ? i + 1 : e.key === 'ArrowUp' ? i - 1 : e.key === 'Home' ? 0 : e.key === 'End' ? items.length - 1 : -1; + var j = e.key === 'ArrowDown' || e.key === 'ArrowRight' ? i + 1 : e.key === 'ArrowUp' || e.key === 'ArrowLeft' ? i - 1 : e.key === 'Home' ? 0 : e.key === 'End' ? items.length - 1 : -1; if (j < 0 || j >= items.length) return; e.preventDefault(); items[j].focus(); showPage(items[j].dataset.page); }); @@ -518,7 +695,7 @@ if (typeof document !== 'undefined') (function () { // address options var opts = document.querySelectorAll('.option'); - function pickOption(el) { opts.forEach(function (o) { o.classList.toggle('on', o === el); }); el.querySelector('input').checked = true; if (el.id === 'opt-paste') $('addr-input').focus(); } + function pickOption(el) { opts.forEach(function (o) { o.classList.toggle('on', o === el); }); el.querySelector('input').checked = true; if (el.id === 'opt-paste') $('addr-input').focus(); setText('btn-start', el.id === 'opt-paste' ? 'Start mining' : 'Continue'); } opts.forEach(function (o) { o.addEventListener('click', function (e) { if (e.target.tagName !== 'INPUT' || e.target.type === 'radio') pickOption(o); }); }); $('addr-input').addEventListener('focus', function () { pickOption($('opt-paste')); }); function validAddr(a) { return /^0x[0-9a-fA-F]{40}$/.test((a || '').trim()); } @@ -550,59 +727,133 @@ if (typeof document !== 'undefined') (function () { api('api/key/saved', {}).then(function () { return api('api/start', {}); }).then(function () { $('sheet-key').hidden = true; $('key-private').textContent = ''; show('dashboard'); }); }); - // ---------- Mine: the big button and the GPU switches ---------- + // ---------- disclosures: one chevron per details block ---------- + function setOpen(btn, det, open) { btn.setAttribute('aria-expanded', open ? 'true' : 'false'); btn.classList.toggle('open', open); det.hidden = !open; } + function disclosure(btnId, detId, onOpen) { + $(btnId).addEventListener('click', function () { var open = $(detId).hidden; setOpen($(btnId), $(detId), open); if (open && onOpen) onOpen(); }); + } + disclosure('node-toggle', 'node-details'); + disclosure('pv-toggle', 'pv-details'); + disclosure('addr-toggle', 'addr-details', function () { $('s-address-input').focus(); }); + disclosure('key-toggle', 'key-details'); + $('s-jobs-history-btn').addEventListener('click', function () { setOpen(this, $('s-jobs-history'), $('s-jobs-history').hidden); $('s-jobs-key').hidden = $('s-jobs-history').hidden; }); + + // ---------- Mine: the big button, the rows, the tune controls ---------- $('btn-toggle').addEventListener('click', function () { if (!state) return; var t = View.toggle(state.mining, state.node, state); if (!t.act) return; api('api/' + t.act, {}).then(function () { toast(t.act === 'pause' ? 'Mining stops; the node keeps running' : 'Mining starts'); }); }); - // the GPU switch on a Mine row: the whole list goes to the engine with that card flipped (identities and the cap - // as they are); pending[key] holds the wanted state until the engine agrees, so the row does not flicker back - var pending = {}; + // a row's switch, slider or stepper sends the whole list to the engine (pending[key] holds a flipped switch until + // the engine agrees, so the row does not flicker back) + var pending = {}, cardsT = null, openCards = {}; + function sendCards(msg) { + clearTimeout(cardsT); + cardsT = setTimeout(function () { + var choices = readCardRows($('d-cards')); + if (choices.length) api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(msg || 'Applied; only the changed worker restarts'); else toast(r.error || 'could not change'); }); + }, 400); + } $('d-cards').addEventListener('change', function (e) { - if (!e.target.matches('.gpu-line .switch input') || !state) return; - var key = e.target.closest('.gpu-line').dataset.key, on = e.target.checked; - pending[key] = { on: on, at: Date.now() }; - var choices = shownCards(state.mining.cards).filter(View.present).map(function (c) { var it = { key: c.key, enabled: c.key === key ? on : c.enabled, identities: c.identities || 1 }; if (c.vendor === 'nvidia' && c.power_default_w > 0) it.power_pct = c.power_pct || 80; return it; }); - api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(on ? 'Worker starting on that card' : 'Worker stopped on that card'); else { delete pending[key]; toast(r.error || 'could not change'); } }); + if (!state) return; + if (e.target.matches('.gpu-line > .gl-main .switch input')) { + var key = e.target.closest('.gpu-line').dataset.key, on = e.target.checked; + pending[key] = { on: on, at: Date.now() }; + var choices = shownCards(state.mining.cards).filter(View.present).map(function (c) { var it = { key: c.key, enabled: c.key === key ? on : c.enabled, identities: c.identities || 1 }; if (c.vendor === 'nvidia' && c.power_default_w > 0) it.power_pct = c.power_pct || 80; return it; }); + api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(on ? 'Worker starting on that card' : 'Worker stopped on that card'); else { delete pending[key]; toast(r.error || 'could not change'); } }); + } else if (e.target.matches('.ids input')) sendCards('Identities set; that card’s worker restarts'); }); + $('d-cards').addEventListener('input', function (e) { + if (e.target.matches('input[type=range]')) { + var row = e.target.closest('.gpu-line'), cd = cardOf(row.dataset.key), pct = parseInt(e.target.value, 10); + row.querySelector('.pv').textContent = pct + '% · ' + (cd && cd.power_default_w ? Math.round(cd.power_default_w * pct / 100) + ' W' : ''); + sendCards('Power cap set; pinned until you unpin it'); + } + }); + $('d-cards').addEventListener('click', function (e) { + var b = e.target.closest('button'); if (!b) return; + var row = b.closest('.gpu-line'), key = row ? row.dataset.key : ''; + if (b.dataset.open !== undefined) { openCards[key] = !openCards[key]; applyOpen(row); } + else if (b.dataset.tune !== undefined) api('api/sweep/start', { key: key }).then(function (r) { toast(r && r.ok === false ? (r.error || 'could not start the tune') : 'Tune queued for ' + (cardOf(key) || {}).name); }); + else if (b.dataset.stop !== undefined) api('api/sweep/stop', {}).then(function () { toast('Tune stopped; the card is back where it was'); }); + else if (b.dataset.d) { var inp = b.parentNode.querySelector('input'), v = parseInt(inp.value, 10) || 1; inp.value = Math.max(1, Math.min(64, v + parseInt(b.dataset.d, 10))); sendCards('Identities set; that card’s worker restarts'); } + else if (b.dataset.pin !== undefined) api('api/sweep/pin', { key: key, pinned: b.dataset.pin === '1' }).then(function () { toast(b.dataset.pin === '1' ? 'Cap pinned' : 'The tune chooses the cap again'); }); + else if (b.dataset.powerRetry !== undefined) api('api/power/apply', {}).then(function () { toast('Administrator prompt: allow it to set the cap'); }); + else if (b.dataset.price !== undefined) { showPage('settings'); $('s-price').focus(); } + }); + function applyOpen(row) { var open = !!openCards[row.dataset.key], det = row.querySelector('.gl-details'), chev = row.querySelector('[data-open]'); if (det) det.hidden = !open; if (chev) { chev.setAttribute('aria-expanded', open ? 'true' : 'false'); chev.classList.toggle('open', open); } row.classList.toggle('open', open); } + function cardOf(key) { return state ? state.mining.cards.filter(function (c) { return c.key === key; })[0] : null; } + $('btn-tune-all').addEventListener('click', function () { + if (!state) return; + var list = shownCards(state.mining.cards).filter(function (c) { return View.tunable(c) && c.enabled; }); + if (!list.length) { toast('No card to tune'); return; } + var n = 0; + list.reduce(function (p, c) { return p.then(function () { return api('api/sweep/start', { key: c.key }).then(function (r) { if (!r || r.ok !== false) n++; }); }); }, Promise.resolve()).then(function () { toast(n === 1 ? 'Tune queued for 1 card' : 'Tune queued for ' + n + ' cards, one at a time'); }); + }); + // the goal (ember-tune: POST api/tune/goal); a build without the route says so and keeps the old value + function setGoal(goal) { + api('api/tune/goal', { goal: goal }).then(function (r) { if (r && r.ok) { store('igneum.goal', goal); toast('Goal: ' + View.GOALS[goal]); } else toast('The goal needs the Ember Tune update'); if (state) renderTuneCard(state); }).catch(function () { toast('The goal needs the Ember Tune update'); }); + } + ['goal-seg', 'goal-seg-2'].forEach(function (id) { $(id).addEventListener('click', function (e) { var b = e.target.closest('.seg-b'); if (b) setGoal(b.dataset.goal); }); }); + function powerControl(on) { api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on: Windows asks for administrator rights once' : 'Power control off; nothing asks'); }); } + $('m-power-control').addEventListener('change', function () { powerControl(this.checked); }); + $('s-power-control').addEventListener('change', function () { powerControl(this.checked); }); + $('m-clock-sync').addEventListener('click', function () { api('api/clock/sync', {}); }); + $('btn-open-log').addEventListener('click', function () { setDrawer(true); }); - // ---------- Prove ---------- - $('s-prove').addEventListener('change', function () { var on = this.checked; api('api/prove', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Proving on; the first shard arrives within a minute' : 'Proving off'); else { toast(r.error || 'could not change'); $('s-prove').checked = !on; } }); }); - $('pv-setup').addEventListener('click', function () { api('api/prove/setup', {}).then(function (r) { toast(r.ok ? 'Setup started in its own window' : (r.error || 'could not start')); }); }); - - // ---------- Rewards ---------- + // ---------- Earnings ---------- + $('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); }); + $('e-price-btn').addEventListener('click', function () { showPage('settings'); $('s-price').focus(); }); + $('t-money-cell').addEventListener('click', function () { if ($('t-money').classList.contains('ask-price')) { showPage('settings'); $('s-price').focus(); } }); $('s-address-save').addEventListener('click', function () { var a = $('s-address-input').value.trim(); if (!validAddr(a)) { $('s-address-err').textContent = 'An address is 0x followed by 40 hex characters.'; $('s-address-err').hidden = false; return; } - api('api/settings', { address: a }).then(function (r) { if (r.ok) { $('s-address-err').hidden = true; $('s-address-input').value = ''; toast('Rewards address changed'); } else { $('s-address-err').textContent = r.error; $('s-address-err').hidden = false; } }); + $('s-address-err').hidden = true; + $('ask-address-text').textContent = View.addressAsk(a); $('ask-address').hidden = false; + }); + $('ask-address-no').addEventListener('click', function () { $('ask-address').hidden = true; }); + $('ask-address-yes').addEventListener('click', function () { + var a = $('s-address-input').value.trim(); + api('api/settings', { address: a }).then(function (r) { $('ask-address').hidden = true; if (r.ok) { $('s-address-input').value = ''; setOpen($('addr-toggle'), $('addr-details'), false); toast('Rewards address changed'); } else { $('s-address-err').textContent = r.error; $('s-address-err').hidden = false; } }); }); $('s-reveal').addEventListener('click', function () { - api('api/key/reveal', {}).then(function (r) { if (r.ok) { $('s-key').textContent = r.private_key; $('s-key-box').hidden = false; $('s-hide').hidden = false; $('s-reveal').hidden = true; } else toast(r.error || 'no key'); }); + api('api/key/reveal', {}).then(function (r) { if (r.ok) { $('s-key').textContent = r.private_key; $('s-key-box').hidden = false; $('ask-key').hidden = true; } else toast(r.error || 'no key'); }); }); - function hideKey() { $('s-key-box').hidden = true; $('s-key').textContent = ''; $('s-hide').hidden = true; $('s-reveal').hidden = false; } + function hideKey() { $('s-key-box').hidden = true; $('s-key').textContent = ''; $('ask-key').hidden = false; } $('s-hide').addEventListener('click', hideKey); + $('ask-key-no').addEventListener('click', function () { setOpen($('key-toggle'), $('key-details'), false); }); $('r-wallet').addEventListener('click', function () { api('api/open', { url: 'https://igneum.network/wallet' }); }); - // ---------- Node ---------- - $('n-clock-sync').addEventListener('click', function () { api('api/clock/sync', {}); }); + // ---------- Prove ---------- + $('s-prove').addEventListener('change', function () { var on = this.checked; api('api/prove', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Proving on; the first piece arrives within a minute' : 'Proving off'); else { toast(r.error || 'could not change'); $('s-prove').checked = !on; } }); }); + $('pv-setup').addEventListener('click', function () { api('api/prove/setup', {}).then(function (r) { toast(r.ok ? 'Setup started in its own window' : (r.error || 'could not start')); }); }); - // ---------- Updates ---------- + // ---------- Settings ---------- $('s-update').addEventListener('click', function () { api('api/update/check', {}); setText('s-update-note', 'Checking.'); }); $('s-install').addEventListener('click', function () { api('api/update/install', {}); toast('Installing at once'); }); $('s-auto-update').addEventListener('change', function () { api('api/update/auto', { on: this.checked }); }); $('s-jobs-check').addEventListener('click', function () { api('api/jobs/check', {}); setText('s-jobs-note', 'Checking.'); }); - - // ---------- Settings ---------- $('s-name-save').addEventListener('click', function () { api('api/settings', { display_name: $('s-name').value }).then(function (r) { if (r.ok) toast('Renamed'); }); }); $('s-vote').addEventListener('change', function () { api('api/settings', { vote: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); }); - $('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); }); $('s-login').addEventListener('change', function () { var on = this.checked; api('api/settings', { start_at_login: on }).then(function (r) { if (!r.ok) { toast(r.error || 'could not change'); $('s-login').checked = !on; } }); }); $('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); }); - $('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Ember Tune on: once after install, then weekly' : 'Ember Tune off'); }); }); - $('s-power-control').addEventListener('change', function () { var on = this.checked; api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on: Windows asks for administrator rights once' : 'Power control off; nothing asks'); }); }); - $('s-trust').addEventListener('change', function () { var on = this.checked; api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); else { toast(r.error || 'could not change'); $('s-trust').checked = !on; } }); }); + $('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Ember Tune on: once after install, then every 7 days' : 'Ember Tune off'); }); }); + $('s-climb').addEventListener('change', function () { var on = this.checked; api('api/tune/goal', { climb: on }).then(function (r) { if (!r || !r.ok) { toast('Needs the Ember Tune update'); $('s-climb').checked = !on; } }); }); + function savePrice() { + var p = parseFloat($('s-price').value); + if (!(p >= 0)) { toast('A price in pence per kWh, for example 28'); return; } + store('igneum.price', p > 0 ? p : null); + api('api/tune/goal', { price_pence: p }).then(function () { toast(p > 0 ? 'Price saved: ' + p + 'p per kWh' : 'Price cleared'); if (state) renderPage(state); }).catch(function () { toast(p > 0 ? 'Price saved on this window: ' + p + 'p per kWh' : 'Price cleared'); if (state) renderPage(state); }); + } + $('s-price-save').addEventListener('click', savePrice); + $('s-price').addEventListener('keydown', function (e) { if (e.key === 'Enter') savePrice(); }); + $('s-trust').addEventListener('change', function () { var on = this.checked; $('s-trust').checked = !on; $('ask-trust-text').textContent = View.trustAsk(on); $('ask-trust').hidden = false; $('ask-trust').dataset.on = on ? '1' : ''; }); + $('ask-trust-no').addEventListener('click', function () { $('ask-trust').hidden = true; }); + $('ask-trust-yes').addEventListener('click', function () { + var on = $('ask-trust').dataset.on === '1'; $('ask-trust').hidden = true; + api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) { $('s-trust').checked = on; toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); } else toast(r.error || 'could not change'); }); + }); $('s-live').addEventListener('click', function () { if (state && state.live_page) api('api/open', { url: state.live_page }); }); $('s-log-open').addEventListener('click', function () { setDrawer(true); }); // the log export: the last lines the engine holds (the ring keeps 3,000; one call returns up to 2,000), to the clipboard @@ -613,41 +864,13 @@ if (typeof document !== 'undefined') (function () { copyText('Igneum Miner ' + (state ? state.version : '') + ' log, ' + lines.length + ' lines\n' + lines.join('\n'), 'Copied ' + lines.length + ' lines'); }).catch(function () { toast('The engine did not answer'); }); }); - // the per-card controls on Settings: the cap slider and the identities stepper send the whole list (debounced), - // the sweep buttons send their own calls - var cardsT = null; - function sendCards(msg) { - clearTimeout(cardsT); - cardsT = setTimeout(function () { - var choices = readCardRows($('s-cards')); - if (choices.length) api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(msg || 'Applied; only the changed worker restarts'); }); - }, 500); - } - $('s-cards').addEventListener('input', function (e) { - if (e.target.matches('input[type=range]')) { - var row = e.target.closest('.set-card'), cd = cardOf(row.dataset.key), pct = parseInt(e.target.value, 10); - row.querySelector('.pv').textContent = pct + '% · ' + (cd && cd.power_default_w ? Math.round(cd.power_default_w * pct / 100) + ' W' : ''); - sendCards('Power cap set; pinned until you unpin it'); - } - }); - $('s-cards').addEventListener('change', function (e) { if (e.target.matches('.ids input')) sendCards('Identities set; that card’s worker restarts'); }); - $('s-cards').addEventListener('click', function (e) { - var b = e.target.closest('button'); if (!b) return; - if (b.dataset.d) { var inp = b.parentNode.querySelector('input'), v = parseInt(inp.value, 10) || 1; inp.value = Math.max(1, Math.min(64, v + parseInt(b.dataset.d, 10))); sendCards('Identities set; that card’s worker restarts'); } - else if (b.dataset.sweepStart) api('api/sweep/start', { key: b.dataset.sweepStart }).then(function () { toast('Tune queued: the power limit and the core clock, 75 s a step'); }); - else if (b.dataset.sweepStop) api('api/sweep/stop', {}).then(function () { toast('Tune stopped; the card is back where it was'); }); - else if (b.dataset.sweepPin) api('api/sweep/pin', { key: b.dataset.sweepPin, pinned: b.dataset.pinned === '1' }).then(function () { toast(b.dataset.pinned === '1' ? 'Cap pinned' : 'The sweep chooses the cap again'); }); - else if (b.dataset.powerRetry) api('api/power/apply', {}).then(function () { toast('Administrator prompt: allow it to set the cap'); }); - }); - function cardOf(key) { return state ? state.mining.cards.filter(function (c) { return c.key === key; })[0] : null; } - // ---------- rail: logs and quit ---------- + // ---------- rail: logs and quit (the quit question is a strip, never a dialog) ---------- $('btn-logs').addEventListener('click', function () { setDrawer(!document.body.classList.contains('drawer-open')); }); $('log-close').addEventListener('click', function () { setDrawer(false); }); - $('btn-quit').addEventListener('click', function () { - if (!confirm('Quit Igneum Miner? The miner stops first, then the node.')) return; - api('api/quit', {}); $('pill-text').textContent = 'stopping'; $('btn-quit').disabled = true; - }); + $('btn-quit').addEventListener('click', function () { $('ask-quit').hidden = false; $('ask-quit-yes').focus(); }); + $('ask-quit-no').addEventListener('click', function () { $('ask-quit').hidden = true; }); + $('ask-quit-yes').addEventListener('click', function () { $('ask-quit').hidden = true; api('api/quit', {}); $('pill-text').textContent = 'stopping'; $('btn-quit').disabled = true; }); // ---------- log drawer ---------- // logLines: everything fetched this session (newest last, capped at LOG_CAP). view: the lines after the source chip @@ -844,28 +1067,31 @@ if (typeof document !== 'undefined') (function () { } function scheduleLogPoll() { clearTimeout(logTimer); logTimer = setTimeout(function () { pollLog(); if (drawerOpen()) scheduleLogPoll(); }, 1500); } + + // ---------- the blocks strip (your blocks over the last 10 minutes) ---------- // The minute grid never moves (it hangs off "now", which is always the right edge), so it is drawn once per size // into an offscreen layer. A frame is clear + the layer + the rail + the blocks. Blocks drift 1.7 px/s, so the // strip redraws twice a second; while a block flashes (1.2 s after it appears) it runs at the display rate. Nothing // draws while the window is hidden (the hosts hide it to the menu bar or the tray). var c = $('dag'), x = c.getContext('2d'), W = 0, H = 0, born = {}, grid = document.createElement('canvas'), gx = grid.getContext('2d'); - var SPAN = 600, S = 18, PAD = 10, RIGHT_PAD = 26; + var SPAN = 600, S = 12, PAD = 10, RIGHT_PAD = 26; + function css(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } function size() { W = c.clientWidth; H = c.clientHeight; if (!W || !H) return; c.width = W * dpr; c.height = H * dpr; x.setTransform(dpr, 0, 0, dpr, 0, 0); grid.width = W * dpr; grid.height = H * dpr; gx.setTransform(dpr, 0, 0, dpr, 0, 0); gx.clearRect(0, 0, W, H); - var right = W - RIGHT_PAD, left = PAD, pxs = (right - left) / SPAN; + var right = W - RIGHT_PAD, left = PAD, pxs = (right - left) / SPAN, line = css('--line') || '#2A2A30', ash = css('--ash') || '#9A9A9E', molten = css('--molten') || '#FFB35C'; gx.font = '500 10px IBM Plex Mono, monospace'; gx.textAlign = 'center'; for (var m = 0; m <= 10; m++) { var px = right - m * 60 * pxs; - gx.strokeStyle = m === 0 ? 'rgba(255,179,92,0.5)' : 'rgba(42,42,48,0.9)'; gx.lineWidth = 1; gx.setLineDash(m === 0 ? [] : [3, 6]); - gx.beginPath(); gx.moveTo(px, 14); gx.lineTo(px, H - 18); gx.stroke(); gx.setLineDash([]); - if (px > 30) { gx.fillStyle = 'rgba(154,154,158,0.8)'; gx.fillText(m === 0 ? 'now' : '-' + m + ' min', px, H - 5); } + gx.strokeStyle = m === 0 ? molten : line; gx.lineWidth = 1; gx.setLineDash(m === 0 ? [] : [3, 6]); + gx.beginPath(); gx.moveTo(px, 10); gx.lineTo(px, H - 18); gx.stroke(); gx.setLineDash([]); + if (px > 30 && (m === 0 || m % 2 === 0)) { gx.fillStyle = ash; gx.fillText(m === 0 ? 'now' : '-' + m + ' min', px, H - 5); } } - var rail = H * 0.5; - gx.strokeStyle = 'rgba(58,58,66,0.9)'; gx.lineWidth = 1; gx.beginPath(); gx.moveTo(left, rail); gx.lineTo(right, rail); gx.stroke(); + var rail = (H - 12) * 0.5; + gx.strokeStyle = line; gx.lineWidth = 1; gx.beginPath(); gx.moveTo(left, rail); gx.lineTo(right, rail); gx.stroke(); requestDraw(); } window.addEventListener('resize', function () { size(); }); @@ -879,24 +1105,24 @@ if (typeof document !== 'undefined') (function () { var right = W - RIGHT_PAD, pxs = (right - PAD) / SPAN; var nowU = state ? (state.now + (performance.now() - stateAt) / 1000) : Date.now() / 1000; var found = state ? state.mining.found : []; - var rail = H * 0.5, shown = 0, animating = false; + var rail = (H - 12) * 0.5, shown = 0, animating = false; for (var i = 0; i < found.length; i++) { var t = found[i], age = nowU - t; if (age < 0 || age > SPAN) continue; shown++; var key = String(t); if (!born[key]) born[key] = age < 5 ? now : now - 2000; // only a block that just arrived flashes, not the whole strip on first paint var fl = Math.max(0, 1 - (now - born[key]) / 1200); if (fl > 0) animating = true; - var px = right - age * pxs, py = rail + ((i % 3) - 1) * 22; + var px = right - age * pxs, py = rail + ((i % 3) - 1) * 15; if (fl > 0) { var g = x.createRadialGradient(px, py, 0, px, py, S * 2.2); g.addColorStop(0, 'rgba(255,179,92,' + (0.6 * fl) + ')'); g.addColorStop(1, 'rgba(255,179,92,0)'); x.fillStyle = g; x.beginPath(); x.arc(px, py, S * 2.2, 0, Math.PI * 2); x.fill(); } - x.fillStyle = '#F2541B'; rrect(px - S / 2, py - S / 2, S, S, 5); x.fill(); - if (fl > 0) { x.strokeStyle = 'rgba(255,179,92,' + fl + ')'; x.lineWidth = 2; rrect(px - S / 2 - 2, py - S / 2 - 2, S + 4, S + 4, 6); x.stroke(); } + x.fillStyle = '#F2541B'; rrect(px - S / 2, py - S / 2, S, S, 3); x.fill(); + if (fl > 0) { x.strokeStyle = 'rgba(255,179,92,' + fl + ')'; x.lineWidth = 2; rrect(px - S / 2 - 2, py - S / 2 - 2, S + 4, S + 4, 4); x.stroke(); } } // forget flashes older than the strip, so the map does not grow for a day-long run if (found.length && Object.keys(born).length > found.length + 50) { var keep = {}; for (var k = 0; k < found.length; k++) if (born[String(found[k])]) keep[String(found[k])] = born[String(found[k])]; born = keep; } if (!shown) { - x.fillStyle = 'rgba(154,154,158,0.9)'; x.font = '500 12px IBM Plex Mono, monospace'; x.textAlign = 'center'; - var msg = !state ? 'waiting for the engine' : state.mining.state === 'mining' ? 'mining: your blocks appear here as the node accepts them' : state.mining.state === 'paused' ? 'paused' : state.node.synced ? 'waiting for the worker' : 'waiting for the node to sync'; - x.fillText(msg, W / 2, rail - 14); + x.fillStyle = css('--ash') || '#9A9A9E'; x.font = '500 12px IBM Plex Mono, monospace'; x.textAlign = 'center'; + var msg = !state ? 'waiting for the engine' : state.mining.state === 'mining' ? 'mining: a block this machine finds appears here' : state.mining.state === 'paused' ? 'paused: no blocks while mining is stopped' : state.node.synced ? 'waiting for the worker' : 'waiting for the node to sync'; + x.fillText(msg, W / 2, rail - 10); } return animating && !reduce; } @@ -914,6 +1140,7 @@ if (typeof document !== 'undefined') (function () { if (hiddenNow()) { clearTimeout(drawTimer); drawTimer = null; if (drawRaf) { cancelAnimationFrame(drawRaf); drawRaf = 0; } clearTimeout(logTimer); logTimer = null; } else { poll(); requestDraw(); if (drawerOpen()) { pollLog(); scheduleLogPoll(); } } }); + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function () { size(); }); requestDraw(); // ---------- render ---------- @@ -930,7 +1157,7 @@ if (typeof document !== 'undefined') (function () { var removed = cd.removed_at > 0, unusable = !!cd.problem; d.className = 'gpu-row' + (cd.enabled && !removed && !unusable ? '' : ' off') + (removed ? ' removed' : '') + (unusable ? ' unusable' : ''); d.dataset.key = cd.key; - var meta = 'worker ' + esc(cd.worker) + '' + (vramText(cd) ? '' + esc(vramText(cd)) + '' : '') + (cd.detail && cd.kind !== 'apple' ? '' + esc(cd.detail) + '' : '') + (cd.detail && cd.kind === 'apple' ? '' + esc(cd.detail.split(',')[0]) + '' : '') + (cd.device ? 'device ' + esc(cd.device) + '' : ''); + var meta = (vramText(cd) ? '' + esc(vramText(cd)) + '' : '') + (cd.detail && cd.kind !== 'apple' ? '' + esc(cd.detail) + '' : '') + (cd.detail && cd.kind === 'apple' ? '' + esc(cd.detail.split(',')[0]) + '' : ''); d.innerHTML = '
' + badgeHtml(cd) + '
' + '
' + esc(cd.name) + (unusable ? ': ' + esc(cd.message || 'not usable (' + cd.problem + ')') : removed ? ': removed' : '') + ' ' + kindWord(cd.kind) + '
' + meta + '
' + (cd.reason ? '
' + esc(cd.reason) + '
' : '') + (cd.message && !unusable && !removed ? '
' + esc(cd.message) + '
' : '') + '
' + @@ -941,12 +1168,12 @@ if (typeof document !== 'undefined') (function () { // what a container's rows say now, for api/cards: a row without a stepper or a slider keeps the engine's value function readCardRows(container) { var out = []; - container.querySelectorAll('.gpu-row, .set-card').forEach(function (r) { + container.querySelectorAll('.gpu-row, .gpu-line').forEach(function (r) { if (r.classList.contains('removed') || r.classList.contains('unusable')) return; var cd = cardOf(r.dataset.key) || {}; var idsInput = r.querySelector('.ids input'), n = idsInput ? parseInt(idsInput.value, 10) : (cd.identities || 1); if (!(n >= 1)) n = 1; if (n > 64) n = 64; - var sw = r.querySelector('.switch input'), pr = r.querySelector('input[type=range]'); + var sw = r.querySelector('.gl-main .switch input, :scope > .switch input'), pr = r.querySelector('input[type=range]'); var item = { key: r.dataset.key, enabled: sw ? sw.checked : !!cd.enabled, identities: n }; if (pr) item.power_pct = parseInt(pr.value, 10); else if (cd.vendor === 'nvidia' && cd.power_default_w > 0) item.power_pct = cd.power_pct || 80; out.push(item); @@ -962,17 +1189,17 @@ if (typeof document !== 'undefined') (function () { if (s.detecting) { det.hidden = false; $('detect-text').textContent = 'asking the graphics cards to report in'; $('btn-cards-next').disabled = true; $('cards-sub').textContent = 'Asking the graphics cards to report in.'; cardsRendered = ''; return; } det.hidden = true; var cards = shownCards(s.mining.cards); - var sig = cards.map(function (c) { return c.key + ':' + (c.problem || '') + ':' + (c.removed_at > 0 ? 'r' : '') + ':' + (c.enabled ? 'on' : 'off'); }).join('|'); // cards is already in performance order, so a reorder changes the signature + var sig = cards.map(function (c) { return c.key + ':' + (c.problem || '') + ':' + (c.removed_at > 0 ? 'r' : '') + ':' + (c.enabled ? 'on' : 'off'); }).join('|'); if (sig !== cardsRendered) { cardsRendered = sig; renderCardRows(list, cards); } if (!cards.length) { - $('cards-sub').textContent = 'No GPU this app can drive was found.'; + $('cards-sub').textContent = 'No graphics card this app can drive was found.'; $('cards-note').textContent = (s.detect_message || '') + ' The node can still run on its own; mining needs a supported card.'; $('cards-note').hidden = false; $('cards-help').hidden = true; $('btn-cards-retry').hidden = false; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = 'Continue without mining'; return; } - $('cards-sub').textContent = cards.length === 1 ? 'One card found. Switch it on to mine with it.' : cards.length + ' GPUs found. Switch on the ones you want to mine with; each gets its own worker.'; + $('cards-sub').textContent = cards.length === 1 ? 'One card found. It mines when it is on.' : cards.length + ' cards found. Switch on the ones you want to mine with.'; $('cards-note').hidden = !s.detect_message; $('cards-note').textContent = s.detect_message || ''; - $('cards-help').hidden = false; + $('cards-help').hidden = !cards.some(function (c) { return c.kind === 'integrated'; }); var nv = cards.filter(function (c) { return c.vendor === 'nvidia' && c.power_default_w > 0; }).length; $('cards-power').hidden = !nv; $('btn-cards-retry').hidden = true; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = 'Continue'; @@ -993,113 +1220,129 @@ if (typeof document !== 'undefined') (function () { // ---------- Mine ---------- var mineRowsSig = '', lastEventsKey = ''; - function gpuLineHtml(r) { - var num = function (cls, k, v, unit, tone) { return '
' + k + '' + (v ? esc(v) + (unit ? '' + unit + '' : '') : 'n/a') + '
'; }; + function numCell(cls, v, unit, k, tone) { return '
' + esc(v) + (unit ? '' + unit + '' : '') + '' + (k ? '' + esc(k) + '' : '') + '
'; } + function gpuLineHtml(cd, s, r) { + var cells = View.cells(cd, priceNow()), tw = View.tuneWords(cd, s, s.now), word = View.rowWord(r, s.mining); + var nums = ''; + if (r.hash) nums += numCell('hash', r.hash, 'MH/s', '', ''); + if (cells.power) nums += numCell('power', cells.power.replace(' W', ''), 'W', cells.eff, ''); + if (cells.money) nums += numCell('money', cells.money, '', 'a day', ''); + else if (cells.needsPrice) nums += '
£ a day
'; + if (r.temp) nums += numCell('temp', r.temp.replace(' °C', ''), '°C', 'GPU', r.tempTone); + var tuneBtn = tw.button === 'stop' ? '' : tw.button === 'tune' ? '' : ''; + var sw = ''; + var chev = ''; + var subs = []; + if (r.sub) subs.push('
' + esc(r.sub) + '
'); + if (cells.noReading && r.on) subs.push('
' + esc(cells.noReading) + '
'); + var tune = tw.kind === 'none' ? '' : '
' + esc(tw.text) + '' + (tw.note ? '' + esc(tw.note) + '' : '') + (tw.progress >= 0 ? '' : '') + '
'; return '
' + - '
' + badgeHtml({ vendor: r.vendor }) + '
' + + '
' + badgeHtml({ vendor: r.vendor }) + '
' + '
' + esc(r.name) + ' ' + esc(r.kindWord) + '
' + - '' + esc(r.word) + (r.meta ? ' · ' + esc(r.meta) : '') + '' + (r.sub ? '
' + esc(r.sub) + '
' : '') + '
' + - num('hash', 'hash rate', r.hash, 'MH/s', '') + num('temp', 'temperature', r.temp, '', r.tempTone) + num('power', 'power', r.power, '', '') + - '
'; + '' + esc(word) + (r.meta ? ' · ' + esc(r.meta) : '') + '' + subs.join('') + '
' + + '
' + nums + '
' + + '
' + tuneBtn + sw + chev + '
' + tune + + ''; + } + // the card's details (one chevron): the cap slider, identities, pin, the telemetry, the facts, the last tune's table + function detailsHtml(cd, s) { + var nv = cd.vendor === 'nvidia' && cd.power_default_w > 0, pct = cd.power_pct || 80, h = ''; + if (cd.removed_at > 0) return '

The card was unplugged; its worker stopped. The row goes in five minutes.

'; + if (cd.problem) return '

' + esc(cd.reason || 'No worker can drive this card.') + ' Reboot with the card attached; if it persists, reinstall the driver with the card attached.

'; + if (nv) { + var want = Math.round(cd.power_default_w * pct / 100); + h += '
Power cap' + pct + '% · ' + want + ' W' + + '

Lower is cooler and quieter; most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so the tune leaves it. ' : 'Ember Tune may move it.') + '

'; + h += cd.power_applied ? '
cap applied: ' + want + ' W' + (cd.sweep_pct === pct && cd.sweep_pct ? ' chosen by the tune' : '') + '
' : '
cap not applied: Windows needs the administrator prompt
'; + } else if (cd.vendor === 'apple') h += '

Apple silicon manages its own power; there is no cap to set.

'; + var tl = []; + if (cd.temp_mem > 0) tl.push('memory ' + Math.round(cd.temp_mem) + ' °C'); + if (cd.gclk_mhz > 0) tl.push('core clock ' + Math.round(cd.gclk_mhz) + ' MHz' + (cd.clock_cap_mhz > 0 ? ' (cap ' + cd.clock_cap_mhz + ')' : '')); + if (cd.mclk_mhz > 0) tl.push('memory clock ' + Math.round(cd.mclk_mhz) + ' MHz' + (cd.mem_cap_mhz > 0 ? ' (set ' + cd.mem_cap_mhz + ')' : '')); + if (cd.fan_pct > 0) tl.push('fan ' + Math.round(cd.fan_pct) + '%'); + if (cd.util_pct > 0) tl.push('busy ' + Math.round(cd.util_pct) + '%'); + if (tl.length) h += '
' + tl.join('') + '
'; + var tm = TuneLine.model(cd, s.now); + if (tm.kind === 'tuned' || tm.kind === 'measured') h += '
' + esc(tm.text) + '' + (tm.note ? ' ' + esc(tm.note) + '' : '') + '
'; + if (cd.tune_curve && cd.tune_curve.length) { + h += '' + cd.tune_curve.map(function (r, i) { return ''; }).join('') + '
stepclockpowerMH/sWMH/Wnote
' + (i + 1) + '' + (r.clock_mhz ? r.clock_mhz + ' MHz' : 'unlocked') + '' + (r.power_pct || 0) + '%' + (r.mhs || 0).toFixed(1) + '' + Math.round(r.watts || 0) + '' + (r.eff || 0).toFixed(3) + '' + esc(r.mark || '') + '
'; + } + h += '
Identities

Each identity votes and is paid on its own. 8 suits a big card, 2 a small one, 1 an integrated GPU. Changing it restarts that card’s worker.

'; + var facts = [cd.worker ? 'worker ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean); + if (facts.length) h += '

' + esc(facts.join(' · ')) + '

'; + return h; } function renderMine(s) { var m = s.mining, n = s.node, p = s.program; var t = View.toggle(m, n, s), tb = $('btn-toggle'); tb.className = 'toggle-big' + (t.cls ? ' ' + t.cls : ''); tb.disabled = t.disabled; setText('btn-toggle-text', t.label); setText('btn-toggle-sub', t.sub); - setText('d-hash', m.hash_total >= 100 ? m.hash_total.toFixed(0) : m.hash_total.toFixed(1)); var shown = shownCards(m.cards), presentCards = shown.filter(View.present); var mining = presentCards.filter(function (c) { return c.state === 'mining'; }).length; - setText('d-hash-sub', m.state === 'paused' ? 'paused' : mining ? (mining + ' of ' + presentCards.length + ' card' + (presentCards.length === 1 ? '' : 's') + ' mining') : m.state === 'waiting' ? (n.synced ? 'worker starting' : 'waiting for the node') : m.state === 'idle' ? 'no GPU worker' : m.state); - setText('d-blocks', withCommas(m.accepted_total)); - setText('d-blocks-sub', m.accepted_session + ' this run' + (m.found.length ? ' · ' + m.found.length + ' last hour' : '') + (m.rejected_session ? ' · ' + m.rejected_session + ' rejected' : '')); - if (n.daa > 0 && p.eta_s >= 0) { setText('d-eta', hms(p.eta_s)); setText('d-eta-sub', p.message + (p.epoch_index ? ' (epoch ' + p.epoch_index + ')' : '')); } - else { setText('d-eta', '--:--'); setText('d-eta-sub', n.state === 'syncing' ? 'after the node is synced' : 'waiting for the node'); } - var nowU = s.now; - setText('d-found-10', String(m.found.filter(function (x) { return nowU - x <= 600; }).length)); - setText('d-found-60', String(m.found.length)); - setText('d-fee', withCommas(m.fee_total || 0)); - $('d-fee').title = (m.fee_session || 0) + ' this run; 1 block in 100 pays the miner software\'s dev fee'; - setText('d-chain-blocks', compact(n.blocks)); - // the rows: every card, on or off; rebuilt only when their words change + // the live total: rate, watts, £ a day, blocks + setText('t-hash', m.hash_total >= 100 ? m.hash_total.toFixed(0) : m.hash_total.toFixed(1)); + setText('t-hash-sub', m.state === 'paused' ? 'paused' : mining ? (mining + ' of ' + presentCards.length + ' card' + (presentCards.length === 1 ? '' : 's') + ' mining') : m.state === 'waiting' ? (n.synced ? 'worker starting' : 'waiting for the node') : m.state === 'idle' ? 'no GPU worker' : m.state); + var watts = View.wattsTotal(m.cards), price = priceNow(), eff = watts > 0 && m.hash_total > 0 ? View.effText(m.hash_total / watts) : ''; + setHidden('t-power-cell', !(watts > 0)); setText('t-power', String(Math.round(watts))); setText('t-power-sub', eff || 'drawn now'); + var ppd = View.poundsPerDay(watts, price); + setHidden('t-money-cell', !(watts > 0)); setText('t-money', ppd === null ? 'Set a price' : money(ppd)); $('t-money').classList.toggle('ask-price', ppd === null); setText('t-money-sub', ppd === null ? 'in Settings, pence per kWh' : 'electricity at ' + price + 'p/kWh'); + setText('t-blocks', withCommas(m.accepted_session)); setText('t-blocks-sub', 'this run · ' + withCommas(m.accepted_total) + ' lifetime' + (m.rejected_session ? ' · ' + m.rejected_session + ' rejected' : '')); + // the rows: every card, on or off; rebuilt only when their words change and never under a hand on a slider var rows = shown.map(View.cardRow), now = Date.now(); rows.forEach(function (r) { var pd = pending[r.key]; if (pd) { if (pd.on === r.on || now - pd.at > 8000) delete pending[r.key]; else { r.on = pd.on; r.word = pd.on ? 'starting' : 'stopping'; r.tone = ''; } } }); var on = rows.filter(function (r) { return r.on; }).length; setText('d-cards-eyebrow', s.detecting ? 'detecting' : !shown.length ? 'none found' : on + ' of ' + presentCards.length + ' on'); - var sig = JSON.stringify(rows); - if (sig !== mineRowsSig) { + var tunables = presentCards.filter(function (c) { return View.tunable(c) && c.enabled; }); + setHidden('btn-tune-all', !(tunables.length > 1) || !!(s.settings && s.settings.tuning_off) || !!View.tuningCard(presentCards)); + var box = $('d-cards'), busy = box.querySelector('input[type=range]:active') || (box.contains(document.activeElement) && document.activeElement.tagName === 'INPUT'); + var sig = JSON.stringify([rows, shown.map(function (c) { return [View.tuneWords(c, s, s.now), View.cells(c, price), c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length]; })]); + if (sig !== mineRowsSig && !busy) { mineRowsSig = sig; - $('d-cards').innerHTML = rows.length ? rows.map(gpuLineHtml).join('') : '
' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No GPU this app can drive was found. The node runs on its own.') + '
'; + box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '
' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can drive was found. The node runs on its own.') + '
'; + box.querySelectorAll('.gpu-line').forEach(applyOpen); } - var note = !shown.length && s.detect_message ? s.detect_message : shown.some(function (c) { return c.problem; }) ? 'A card the OS reports a problem on gets no worker. Reboot with the card attached; if it persists, reinstall the driver with the card attached.' : presentCards.some(function (c) { return c.vendor === 'nvidia' && c.power_default_w > 0 && !c.power_applied && c.enabled; }) ? 'A power cap is not applied yet: Settings has a Retry for the administrator prompt.' : ''; + var note = !shown.length && s.detect_message ? s.detect_message : ''; $('d-cards-note').hidden = !note; setText('d-cards-note', note); + renderTuneCard(s); + renderNode(s); + // activity: the counts, the strip, the newest 8 events in a user's words + var nowU = s.now, f10 = m.found.filter(function (x) { return nowU - x <= 600; }).length; + setText('d-stats', f10 + ' block' + (f10 === 1 ? '' : 's') + ' in 10 min · ' + m.found.length + ' in the last hour' + (m.fee_total ? ' · ' + withCommas(m.fee_total) + ' to the dev fee' : '')); var key = s.events.length ? s.events[0].t + ':' + s.events.length : ''; if (key !== lastEventsKey) { lastEventsKey = key; - $('d-events').innerHTML = s.events.length ? s.events.slice(0, 14).map(function (e) { return '
' + esc(e.kind) + '' + esc(e.text) + '' + clock(e.t) + '
'; }).join('') : '
No events yet. The node and the miner report here as they start.
'; + $('d-events').innerHTML = s.events.length ? s.events.slice(0, 8).map(function (e) { return '
' + esc(e.kind) + '' + esc(View.eventLine(e.text)) + '' + clock(e.t) + '
'; }).join('') : '
No events yet. The node and the miner report here as they start.
'; } } - - // ---------- Prove ---------- - function renderProve(s) { - var pv = s.proving || {}, enabled = !!(s.settings && s.settings.prove); - if (document.activeElement !== $('s-prove')) $('s-prove').checked = enabled; - var w = View.proveWords(pv, enabled, s.node.synced); - setText('pv-state', w.word + (pv.backend && enabled && pv.available ? ' (' + pv.backend.toUpperCase() + ')' : '')); - setText('pv-state-sub', w.sub); - var cell = $('pv-state').parentNode; cell.classList.toggle('ok', w.tone === 'on' || w.tone === 'ok'); cell.classList.toggle('bad', w.tone === 'bad'); - // proving v1: when off by the install-time default, the default's own line says why (src/provedefault.rs) - setText('pv-note', (!enabled && pv.default_note) ? 'Off. ' + pv.default_note + '.' : w.note); - setText('pv-assigned', String(pv.assigned || 0)); - setText('pv-submitted', String(pv.submitted || 0)); - setText('pv-paid', String(pv.paid || 0)); - setText('pv-paid-sub', pv.paid_wei ? (Number(pv.paid_wei) / 1e18).toFixed(4) + ' IGN earned' : 'shards paid out'); - // proving v1 segments (6 October 2026): whole segments this machine proved, and the segment path's last line - var segLine = ''; - if (pv.segments_submitted) segLine = 'Segments: ' + pv.segments_submitted + ' proven whole, ' + (pv.segments_paid || 0) + ' paid' + (pv.segment_paid_wei && Number(pv.segment_paid_wei) ? ' (' + (Number(pv.segment_paid_wei) / 1e18).toFixed(4) + ' IGN to the aggregator)' : '') + (pv.segment_last_s ? ', the last in ' + Math.round(pv.segment_last_s) + ' s' : '') + (pv.segments_held ? ', ' + pv.segments_held + ' record' + (pv.segments_held > 1 ? 's' : '') + ' held for the chain rule' : '') + '.'; - if (pv.segment_note && enabled) segLine += (segLine ? ' ' : '') + pv.segment_note + '.'; - $('pv-seg-note').hidden = !segLine; setText('pv-seg-note', segLine); - var v = View.verifierWords(pv); - setText('pv-verifier', v.word); $('pv-verifier').className = 'big-word ' + v.tone; - $('pv-verifier-note').hidden = !pv.verifier_note; setText('pv-verifier-note', pv.verifier_note || ''); - $('pv-setup-row').hidden = !((pv.enabled && !pv.available && pv.setup_hint) || v.needsSetup); - setText('pv-program', pv.program_id || 'not read yet'); - setText('pv-aggregator', pv.aggregator_id || 'not read yet'); + // the Tuning card on Mine and the Tuning rows on Settings share the goal, the schedule and the Power control switch + function renderTuneCard(s) { + var st = s.settings || {}, goal = goalNow(), cards = shownCards(s.mining.cards), gw = View.goalWords(goal, cards, priceNow()); + ['goal-seg', 'goal-seg-2'].forEach(function (id) { document.querySelectorAll('#' + id + ' .seg-b').forEach(function (b) { var on = b.dataset.goal === goal; b.classList.toggle('on', on); b.setAttribute('aria-checked', on ? 'true' : 'false'); }); }); + setText('goal-line', gw.consequence); setText('goal-line-2', gw.consequence); + var sched = View.schedule(s.mining.cards, s, s.now); + setText('tune-schedule', sched); setText('s-tune-schedule', sched); + setText('tune-eyebrow', st.tuning_off ? 'paused by Igneum' : st.sweep === false ? 'Ember Tune off' : 'Ember Tune'); + var nvOff = cards.some(function (c) { return c.vendor === 'nvidia' && View.present(c); }) && !st.power_control; + setHidden('m-power-row', !nvOff); + if (document.activeElement !== $('m-power-control')) $('m-power-control').checked = !!st.power_control; } - // ---------- Rewards ---------- - function renderRewards(s) { - var a = s.address || {}, m = s.mining; - setText('r-address', a.display || 'not set'); - setText('r-source', a.source === 'generated' ? 'made on this machine' : a.source === 'pasted' ? 'your own address' : ''); - setText('r-address-help', a.display ? 'Every block this machine finds pays this address.' : 'No address yet. Finish the setup to choose one.'); - setText('r-blocks', withCommas(m.accepted_total)); - setText('r-session', withCommas(m.accepted_session)); - setText('r-session-sub', 'since the app started, ' + uptime(s.uptime_s) + ' ago'); - var gen = a.source === 'generated'; - setText('r-key-help', gen ? 'The key for this address was made on this machine and is stored in the app folder, readable by your user only. Keep a copy somewhere safe: anyone with the key can spend what the address holds.' : 'You pasted your own address, so its key lives in your wallet, not here. Nothing to back up on this machine.'); - $('r-key-row').hidden = !gen; if (!gen) hideKey(); - setText('r-key-file', gen && a.wallet_file ? a.wallet_file : ''); - setText('r-devfee-line', View.devFeeLine(s)); - } - - // ---------- Node ---------- + // ---------- the node line and its details ---------- var switchesSig = ''; function renderNode(s) { - var n = s.node, f = s.finality, eta = syncEta(n, s.now); - var w = View.nodeWords(n, s.clock, eta); - setText('d-node', w.word); setText('d-node-sub', w.line); - $('d-node-sub').title = w.line + (n.last_reading_age_s >= 0 ? ' · last reading ' + Math.round(n.last_reading_age_s) + ' s ago' : ''); - var cell = $('n-state-cell'); cell.classList.toggle('ok', w.tone === 'ok'); cell.classList.toggle('bad', w.tone === 'bad'); - setText('n-sync-word', w.word); $('n-sync-word').className = 'big-word ' + (w.tone === 'ok' ? 'ok' : w.tone === 'bad' ? 'warn' : ''); - setText('n-sync-eyebrow', n.state === 'restarting' ? 'restart ' + (n.restarts || 0) : n.starts > 1 ? 'start ' + n.starts : ''); - setText('n-note', w.line + (n.override_restart_wait ? ' ' + n.override_restart_wait + '.' : '')); + var n = s.node, f = s.finality, p = s.program, eta = syncEta(n, s.now); + var nl = View.nodeLine(n, s.clock, eta); + setText('node-line-text', nl.text); setText('node-sub', nl.sub); + $('node-dot').className = 'dot' + (nl.tone === 'ok' ? ' live' : nl.tone === 'bad' ? ' bad' : ''); + $('node-card').classList.toggle('bad', nl.tone === 'bad'); + setText('n-state-v', nl.text.replace(/^Node /, '')); setText('n-state-m', nl.sub + (n.override_restart_wait ? ' ' + n.override_restart_wait + '.' : '')); setText('n-blocks', withCommas(n.blocks)); setText('n-blocks-sub', View.heightLine(n)); setText('n-peers', String(n.peers)); setText('n-peers-sub', View.peersLine(n)); setText('n-version', n.version ? n.version.replace(/^igneumd\s*/, '') : '--'); setText('d-node-net', s.chain || s.network || ''); - setText('n-reading', n.state === 'synced' || n.state === 'syncing' ? 'read every 10 s' : ''); setText('n-headers', withCommas(n.headers)); setText('n-daa', withCommas(n.daa)); setText('n-diff', compact(n.difficulty)); setText('n-tips', String(n.tips)); setText('n-blue', withCommas(n.blue)); + if (n.daa > 0 && p && p.eta_s >= 0) { setText('d-eta', hms(p.eta_s)); setText('d-eta-sub', 'until the next hourly mining program' + (p.epoch_index ? ' (epoch ' + p.epoch_index + ')' : '') + (p.message ? ' · ' + p.message : '')); } + else { setText('d-eta', '--:--'); setText('d-eta-sub', n.state === 'syncing' ? 'known after the node is synced' : 'known once the node runs'); } setText('n-digest', n.consensus_digest || (n.state === 'stopped' || n.state === 'failed' ? 'the node is not running' : 'not printed yet')); var sw = View.nextSwitch(n.consensus_switches, n.daa); setText('n-switch', sw ? sw.name + ' at ' + withCommas(sw.daa) : (n.consensus_switch_daa > 0 && n.consensus_switch_daa > n.daa ? 'DAA ' + withCommas(n.consensus_switch_daa) : (n.consensus_switches && n.consensus_switches.length ? 'none ahead' : 'none planned'))); @@ -1109,17 +1352,73 @@ if (typeof document !== 'undefined') (function () { switchesSig = sig; $('n-switches').innerHTML = (n.consensus_switches || []).map(function (x) { return '' + esc(x.name) + (x.daa <= n.daa ? ' (applied)' : '') + '' + withCommas(x.daa) + ''; }).join(''); } - if (f.last_lock > 0) { setText('f-lock', '#' + withCommas(f.last_lock)); setText('f-age', rel(f.age_s)); setText('f-note', 'A lock is a point the miners have agreed can never be undone. Checkpoints lock at 2/3 of the 30-day weight; this machine votes every 30 s.'); } - else { setText('f-lock', 'none yet'); setText('f-age', 'n/a'); setText('f-note', f.message || 'Locks appear once the miner votes on checkpoints.'); } + if (f.last_lock > 0) { setText('f-lock', '#' + withCommas(f.last_lock) + ' · ' + rel(f.age_s)); setText('f-note', 'a point the miners agreed can never be undone; locks at 2/3 of the 30-day weight'); } + else { setText('f-lock', 'none yet'); setText('f-note', f.message || 'locks appear once the miner votes on checkpoints'); } setText('f-votes', withCommas(f.votes)); } - // ---------- Updates ---------- - function renderUpdates(s) { + // ---------- Earnings ---------- + function renderEarnings(s) { + var a = s.address || {}, m = s.mining, pv = s.proving || {}, price = priceNow(); + var ign = pv.paid_wei && Number(pv.paid_wei) ? (Number(pv.paid_wei) / 1e18).toFixed(4) : '0.0000'; + setText('e-ign', ign + ' IGN'); setText('e-ign-sub', 'from ' + (pv.paid || 0) + ' proof' + (pv.paid === 1 ? '' : 's') + (pv.segments_paid ? ' and ' + pv.segments_paid + ' segment' + (pv.segments_paid === 1 ? '' : 's') : '')); + setText('e-blocks', withCommas(m.accepted_total) + ' block' + (m.accepted_total === 1 ? '' : 's')); setText('e-blocks-sub', 'lifetime · ' + withCommas(m.accepted_session) + ' this run, ' + uptime(s.uptime_s)); + var watts = View.wattsTotal(m.cards), ppd = View.poundsPerDay(watts, price); + setText('e-cost', ppd === null ? (watts > 0 ? Math.round(watts) + ' W' : 'no draw') : money(ppd) + ' a day'); + setText('e-cost-sub', ppd === null ? (watts > 0 ? 'electricity: set a price to see the cost' : 'electricity: no card reports its draw') : 'electricity at ' + price + 'p/kWh for ' + Math.round(watts) + ' W'); + setHidden('e-price-btn', !(watts > 0 && ppd === null)); + if (document.activeElement !== $('s-devfee')) $('s-devfee').checked = !!(s.settings && s.settings.dev_fee); + setText('s-devfee-text', s.settings.dev_fee ? 'Dev fee: 1 block in 100 pays the miner software’s author' : 'Dev fee off: every block pays your address'); + setText('r-devfee-line', s.settings.dev_fee ? 'The protocol takes nothing; this is the miner software’s fee, the same way every GPU miner takes one' + (m.fee_total ? ' (' + withCommas(m.fee_total) + ' block' + (m.fee_total === 1 ? '' : 's') + ' so far)' : '') + '.' : 'The protocol takes nothing either.'); + setText('r-address', a.display || 'not set'); + setText('r-source', a.source === 'generated' ? 'made on this machine' : a.source === 'pasted' ? 'your own address' : ''); + setText('r-address-help', a.display ? 'Every block this machine finds pays this address.' : 'No address yet. Finish the setup to choose one.'); + var gen = a.source === 'generated'; + setHidden('key-toggle', !gen); if (!gen) { $('key-details').hidden = true; hideKey(); } + setText('r-key-help', 'The key for this address was made on this machine and is stored in the app folder, readable by your user only. Keep a copy somewhere safe.'); + setText('r-key-file', gen && a.wallet_file ? a.wallet_file : ''); + } + + // ---------- Prove ---------- + var tiersSig = ''; + function renderProve(s) { + var pv = s.proving || {}, enabled = !!(s.settings && s.settings.prove); + if (document.activeElement !== $('s-prove')) $('s-prove').checked = enabled; + var w = View.proveWords(pv, enabled, s.node.synced); + var cards = shownCards(s.mining.cards).filter(View.present), tiers = cards.map(View.proveTier); + var sig = tiers.join('|'); + if (sig !== tiersSig) { tiersSig = sig; $('pv-tiers').innerHTML = tiers.map(function (t) { return '
  • ' + esc(t) + '
  • '; }).join(''); } + setText('pv-line', View.proveLine(pv, enabled, s.node.synced)); + $('pv-line').className = 'line-text ' + (w.tone === 'on' || w.tone === 'ok' ? 'ok' : w.tone === 'bad' ? 'bad' : ''); + setText('pv-note', (!enabled && pv.default_note) ? pv.default_note + '.' : w.note); + var v = View.verifierWords(pv); + setText('pv-verifier', v.word); $('pv-verifier').className = 'v ' + v.tone; + setText('pv-verifier-help', pv.verifier_note || 'the node checks a proof before it counts'); + $('pv-setup-row').hidden = !((pv.enabled && !pv.available && pv.setup_hint) || v.needsSetup); + var seg = pv.segments_submitted ? pv.segments_submitted + ' proven whole, ' + (pv.segments_paid || 0) + ' paid' + (pv.segment_paid_wei && Number(pv.segment_paid_wei) ? ' (' + (Number(pv.segment_paid_wei) / 1e18).toFixed(4) + ' IGN)' : '') + (pv.segment_last_s ? ', the last in ' + Math.round(pv.segment_last_s) + ' s' : '') + (pv.segments_held ? ', ' + pv.segments_held + ' held for the chain rule' : '') : 'none yet'; + setText('pv-seg', seg); setText('pv-seg-note', pv.segment_note && enabled ? pv.segment_note : 'whole segments this machine proved'); + setText('pv-program', pv.program_id || 'not read yet'); + setText('pv-aggregator', pv.aggregator_id || 'not read yet'); + } + + // ---------- Settings ---------- + var jobsSig = ''; + function renderSettings(s) { + var st = s.settings || {}, sw = function (id, on) { if (document.activeElement !== $(id)) $(id).checked = !!on; }; + sw('s-vote', st.vote); sw('s-login', st.start_at_login); sw('s-jobs-allow', s.jobs && s.jobs.allowed); sw('s-sweep', st.sweep); sw('s-trust', st.proof_verify_trust); sw('s-power-control', st.power_control); sw('s-climb', st.tune_climb); + setHidden('s-climb-row', st.tune_climb === undefined); + setText('s-power-note', st.tuning_off ? ' ' + (st.tuning_note || '') : (st.power_note ? ' ' + st.power_note : '')); + if (document.activeElement !== $('s-price')) { var p = priceNow(); $('s-price').value = p > 0 ? String(p) : ''; } + renderTuneCard(s); + if (document.activeElement !== $('s-name')) $('s-name').value = s.display_name || ''; + setText('s-mid', 'machine id ' + (s.machine_id || '').slice(0, 8)); + setText('s-node-dir', 'chain data: ' + s.node_dir); + setText('s-log-dir', 'log files: ' + s.log_dir); + $('s-live').hidden = !s.live_page; var u = s.update || {}; setText('s-version', 'Igneum Miner ' + s.version); setText('s-update-note', settingsUpdateNote(u)); - if (document.activeElement !== $('s-auto-update')) $('s-auto-update').checked = !!(s.settings && s.settings.auto_update); + sw('s-auto-update', st.auto_update); $('s-install').hidden = !(u.version && (u.ready || u.downloaded || u.status === 'error') && !/no update manifest configured/.test(u.error || '') && !u.applying); $('nav-updates-dot').hidden = !(u.available && !u.applying); fillJobs(s.jobs || {}, s.now); @@ -1130,81 +1429,24 @@ if (typeof document !== 'undefined') (function () { var h = j.history || [], sig = JSON.stringify(h); if (sig === jobsSig) return; jobsSig = sig; + $('s-jobs-history-btn').hidden = !h.length; $('s-jobs-history').innerHTML = h.length ? '' + h.map(function (r) { return ''; }).join('') + '
    jobkindstartedexitreport
    ' + esc(r.id) + '' + esc(r.kind) + '' + (r.started_at ? clock(r.started_at) : '') + '' + esc(r.status) + (r.status !== 'running' ? ' (' + r.exit + ')' : '') + '' + (r.uploaded ? 'uploaded' : (r.status === 'running' ? 'pending' : 'not uploaded')) + '
    ' : '

    No job has run on this machine yet.

    '; } - var jobsSig = ''; - - // ---------- Settings ---------- - // the switches follow the state unless that switch has the focus; the per-card block is rebuilt when a card's - // settings change (never while a slider is being dragged) - var setCardsSig = ''; - function fillSettings() { - if (!state) return; - var s = state; - var sw = function (id, on) { if (document.activeElement !== $(id)) $(id).checked = !!on; }; - sw('s-vote', s.settings.vote); sw('s-devfee', s.settings.dev_fee); sw('s-login', s.settings.start_at_login); - sw('s-jobs-allow', s.jobs && s.jobs.allowed); sw('s-sweep', s.settings.sweep); sw('s-trust', s.settings.proof_verify_trust); sw('s-power-control', s.settings.power_control); - setText('s-power-note', s.settings.tuning_off ? s.settings.tuning_note : (s.settings.power_note || (s.settings.power_control ? '' : 'Off: NVIDIA cards measure only; AMD cards need no rights.'))); - setText('s-devfee-text', View.devFeeText(s)); - if (document.activeElement !== $('s-name')) $('s-name').value = s.display_name || ''; - setText('s-mid', (s.machine_id || '').slice(0, 8)); - setText('s-node-dir', 'chain data: ' + s.node_dir); - setText('s-log-dir', 'log files: ' + s.log_dir); - $('s-live').hidden = !s.live_page; - renderSetCards(s); - } - function setCardHtml(cd) { - var nv = cd.vendor === 'nvidia' && cd.power_default_w > 0, pct = cd.power_pct || 80, removed = cd.removed_at > 0, unusable = !!cd.problem; - var h = '
    ' + esc(cd.name) + '' + kindWord(cd.kind) + '' + esc(cd.worker) + (vramText(cd) ? ' · ' + esc(vramText(cd)) : '') + (removed ? ' · removed' : unusable ? ' · not usable (' + esc(cd.problem) + ')' : cd.enabled ? '' : ' · off') + '
    '; - if (removed) return h + '

    The card was unplugged; its worker stopped. The row goes in five minutes.

    '; - if (unusable) return h + '

    ' + esc(cd.reason || 'No worker can drive this card.') + '

    '; - if (nv) { - h += '
    Power cap' + pct + '% · ' + Math.round(cd.power_default_w * pct / 100) + ' W' + - '

    Lower is cooler and quieter; most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so the tune leaves it.' : 'Ember Tune may move it.') + '

    '; - var want = Math.round(cd.power_default_w * pct / 100); - h += cd.power_applied ? '
    cap applied: ' + want + ' W' + (cd.pinned ? ' pinned' : cd.sweep_pct === pct && cd.sweep_pct ? ' chosen by the sweep' : '') + '
    ' : '
    cap not applied (needs the administrator prompt)
    '; - if (cd.telemetry_at > 0) h += '
    draw ' + (cd.power_w ? Math.round(cd.power_w) + ' W' : 'n/a') + 'GPU ' + (cd.temp_gpu ? Math.round(cd.temp_gpu) + ' °C' : 'n/a') + 'memory ' + (cd.temp_mem ? Math.round(cd.temp_mem) + ' °C' : 'n/a') + 'efficiency ' + (cd.eff_mhw ? cd.eff_mhw.toFixed(3) + ' MH/W' : 'n/a') + '
    '; - } else if (cd.vendor === 'apple') { - h += '

    Apple silicon manages its own power; there is no cap to set. Ember Tune measures the card as it runs.

    '; - } - // Ember Tune's line (TuneLine.model): running, tuned, measured, stopped or not run yet, for every card - h += tuneLineHtml(cd); - h += '
    Identities

    Each identity votes and is paid on its own. 8 suits a big card, 2 a small one, 1 an integrated GPU. Changing it restarts that card’s worker.

    '; - return h + ''; - } - function tuneLineHtml(cd) { - var m = TuneLine.model(cd, state ? state.now : 0); - if (m.kind === 'off') return m.text ? '
    ' + esc(m.text) + '
    ' : ''; - var running = m.kind === 'running'; - var t = m.kind === 'tuned' || m.kind === 'measured' ? '' + esc(m.text) + '' + (m.note ? ' ' + esc(m.note) + '' : '') : esc(m.text); - var btn = running ? '' : '' + (cd.pinned ? ' ' : ''); - return '
    ' + t + ' ' + btn + '
    '; - } - function renderSetCards(s) { - var cards = shownCards(s.mining.cards), box = $('s-cards'); - if (box.querySelector('input[type=range]:active') || box.contains(document.activeElement)) return; - var sig = JSON.stringify(cards.map(function (c) { return [c.key, c.enabled, c.identities, c.power_pct, c.pinned, c.power_applied, c.sweep_state, c.sweep_pct, c.sweep_note, c.tune_line, c.tune_source, c.tune_control, Math.round(c.power_w || 0), Math.round(c.temp_gpu || 0), Math.round(c.temp_mem || 0), c.problem || '', c.removed_at > 0]; })); - if (sig === setCardsSig) return; - setCardsSig = sig; - setText('s-cards-eyebrow', cards.length ? cards.length + ' card' + (cards.length === 1 ? '' : 's') : ''); - box.innerHTML = cards.length ? cards.map(setCardHtml).join('') : '
    No card yet. Mine shows what was found.
    '; - } function renderPage(s) { if (page === 'mine') renderMine(s); + else if (page === 'earnings') renderEarnings(s); else if (page === 'prove') renderProve(s); - else if (page === 'rewards') renderRewards(s); - else if (page === 'node') renderNode(s); - else if (page === 'updates') renderUpdates(s); - else if (page === 'settings') fillSettings(); + else if (page === 'settings') renderSettings(s); } function renderPill(s) { var pill = $('pill'), text; pill.classList.remove('on', 'warn'); if (s.quitting) text = 'stopping'; else if (!s.setup_done) text = s.detecting ? 'detecting' : 'set up'; + else if (View.tuningCard(s.mining.cards)) { text = 'tuning'; pill.classList.add('on'); } else if (s.mining.state === 'mining') { text = s.mining.hash_total.toFixed(1) + ' MH/s'; pill.classList.add('on'); } else if (s.mining.state === 'paused') text = 'paused'; else if (s.node.state === 'syncing' || s.node.state === 'starting') text = 'node ' + s.node.state; @@ -1212,26 +1454,22 @@ if (typeof document !== 'undefined') (function () { else if (s.mining.state === 'idle' && s.node.state === 'synced') { text = 'node synced'; pill.classList.add('on'); } else text = s.mining.state; $('pill-text').textContent = text; - // the top line and the rail foot: the machine, the uptime, where rewards go - var nodeWord = s.node.state === 'synced' ? 'synced' : s.node.state; - var top = phase !== 'dashboard' ? '' : s.quitting ? 'stopping: miners first, then the node' : '' + esc(s.mining.state === 'mining' ? 'mining' : s.mining.state) + 'node ' + esc(nodeWord) + 'up ' + uptime(s.uptime_s) + ''; - if (top !== lastTop) { lastTop = top; $('top-status').innerHTML = top; } - var foot = (s.display_name ? '' + esc(s.display_name) + '
    ' : '') + (s.address.display ? 'rewards to ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) : 'no address yet'); + var foot = (s.display_name ? '' + esc(s.display_name) + '
    ' : '') + (s.address.display ? 'pays ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) : 'no address yet') + '
    up ' + uptime(s.uptime_s); if (foot !== lastFoot) { lastFoot = foot; $('rail-status').innerHTML = foot; } var fv = 'v' + esc(s.version) + ' · ' + esc(s.chain || s.network || '') + ''; if (fv !== lastVersion) { lastVersion = fv; $('foot-version').innerHTML = fv; } } - var lastTop = '', lastFoot = '', lastVersion = ''; - // the clock on the Node page (the strip carries it on the setup screens, see Notices) + var lastFoot = '', lastVersion = ''; + // the clock card on Mine (the strip carries it on the setup screens, see Notices) function renderClock(s) { var c = s.clock || { severity: 'none' }, bad = c.severity === 'block', warn = c.severity === 'warn'; - var card = $('n-clock'); + var card = $('m-clock'); card.hidden = !(bad || warn); card.classList.toggle('warn', warn); - setText('n-clock-msg', c.message || ''); - setText('n-clock-hint', c.hint ? 'By hand: ' + c.hint : ''); - setText('n-clock-result', c.sync_result || ''); - $('n-clock-sync').disabled = !!c.syncing; setText('n-clock-sync', c.syncing ? 'Syncing' : 'Sync clock'); + setText('m-clock-msg', c.message || ''); + setText('m-clock-hint', c.hint ? 'By hand: ' + c.hint : ''); + setText('m-clock-result', c.sync_result || ''); + $('m-clock-sync').disabled = !!c.syncing; setText('m-clock-sync', c.syncing ? 'Syncing' : 'Sync clock'); $('btn-start').disabled = bad; $('start-blocked').hidden = !bad; setText('start-blocked', bad ? c.message + ' Use "Sync clock" above.' : ''); @@ -1337,25 +1575,25 @@ if (typeof document !== 'undefined') (function () { else if (act === 'open') api('api/update/open', {}); }); $('upd-more').addEventListener('click', function () { updMore = !updMore; $('upd-all').hidden = !updMore; this.textContent = updMore ? 'Less' : 'What changed'; this.setAttribute('aria-expanded', updMore ? 'true' : 'false'); }); - document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !$('upd').hidden) { e.preventDefault(); cardLater(); } }); + document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { if (!$('upd').hidden) { e.preventDefault(); cardLater(); } else if (!$('ask-quit').hidden) $('ask-quit').hidden = true; } }); - // ---------- over-the-air updates (src/ota.rs): the Updates page's line; the strip is Notices.updateNotice ---------- + // ---------- over-the-air updates (src/ota.rs): the Settings card's line; the strip is Notices.updateNotice ---------- function settingsUpdateNote(u) { var n = Notices.updateNotice(u, state || {}), parts = []; if (u.updated_from) parts.push('Updated from ' + u.updated_from + '.'); if (u.rolled_back) parts.push('Rolled back: ' + u.rolled_back + '.'); if (n && n.kind !== 'update-installed' && !(u.rolled_back && u.status === 'error')) parts.push(n.text + (n.detail ? ' ' + Notices.endDot(n.detail) : '')); else if (u.status === 'checking') parts.push('Checking.'); - else if (u.status === 'current') parts.push('This is the latest version' + (u.checked_at ? ' (checked ' + rel(state.now - u.checked_at) + ')' : '') + '.'); + else if (u.status === 'current') parts.push('Up to date' + (u.checked_at ? ', checked ' + rel(state.now - u.checked_at) : '') + '.'); else if (/no update manifest configured/.test(u.error || '')) parts.push('This build has no update address; updates come as a new download.'); else if (u.error) parts.push(u.error); else if (!u.status || u.status === 'unknown') parts.push('Not checked yet. The engine checks by itself every hour.'); - if (u.activation_height && !u.urgent) parts.push('Consensus upgrade at height ' + withCommas(u.activation_height) + '.'); + if (u.activation_height && !u.urgent && state && state.node && u.activation_height > state.node.daa) parts.push('A rule switch comes at height ' + withCommas(u.activation_height) + '.'); return parts.join(' '); } function sampleUpdate(kind) { // ?update=[&auto=0]: the card and the strip for each state, without an engine - var u = { available: true, version: '0.3.7', notes: 'The update card: one centred card, Install now and Later. Worker restarts no longer drop the hourly program. The log drawer wraps long lines and jumps to the last error. Windows Hello is written but untested; Touch ID confirms every send on the Mac.', status: 'ready', downloaded: true, ready: true, applying: false, progress: 1, size: 20588331, auto: params.get('auto') !== '0', wait: 'installs at the next safe moment', urgent: false, urgent_text: '', activation_height: 0, error: '', updated_from: '', rolled_back: '' }; + var u = { available: true, version: '0.3.14', notes: 'The miner has four sections: Mine, Earnings, Prove, Settings. Every card is a row with its rate, watts, £ a day and a Tune button. The node is one line. Updates are one card.', status: 'ready', downloaded: true, ready: true, applying: false, progress: 1, size: 20588331, auto: params.get('auto') !== '0', wait: 'installs at the next safe moment', urgent: false, urgent_text: '', activation_height: 0, error: '', updated_from: '', rolled_back: '' }; if (!u.auto) u.wait = 'waiting for Install now (automatic updates are off)'; if (kind === 'available') { u.status = 'available'; u.downloaded = false; u.ready = false; u.progress = 0; } if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = Math.min(0.97, 0.43 + ((Date.now() / 1000) % 60) / 110); } @@ -1363,13 +1601,13 @@ if (typeof document !== 'undefined') (function () { if (kind === 'waiting') { u.wait = 'hourly program boundary in 97 s; installing after it'; } if (kind === 'applying') { u.status = 'applying'; u.applying = true; } if (kind === 'deferred') { u.status = 'deferred'; u.wait = 'waits for the next time someone is at this PC (Windows asks for permission); mining continues'; } - if (kind === 'urgent') { u.status = 'downloading'; u.ready = false; u.downloaded = false; u.progress = 0.27; u.urgent = true; u.activation_height = 120000; u.urgent_text = 'Consensus upgrade at height 120000 (difficulty v2): the node is 1,240 blocks away. Installing 0.3.7 now.'; } + if (kind === 'urgent') { u.status = 'downloading'; u.ready = false; u.downloaded = false; u.progress = 0.27; u.urgent = true; u.activation_height = 120000; u.urgent_text = 'Consensus upgrade at height 120000 (difficulty v2): the node is 1,240 blocks away. Installing 0.3.14 now.'; } if (kind === 'manual') { u.status = 'manual'; u.ready = false; u.wait = '/Applications is not writable; open the downloaded disk image and drag the app over the old one'; } if (kind === 'error') { u.status = 'error'; u.error = 'sha256 mismatch: the file is not what the manifest signed'; u.downloaded = false; u.ready = false; } - if (kind === 'updated') { u.status = 'current'; u.available = false; u.ready = false; u.downloaded = false; u.updated_from = '0.3.6'; u.version = '0.3.7'; } + if (kind === 'updated') { u.status = 'current'; u.available = false; u.ready = false; u.downloaded = false; u.updated_from = '0.3.13'; u.version = '0.3.14'; } return u; } - // ---------- remote jobs (src/jobrun.rs): the Updates page; the strip is Notices.jobNotice ---------- + // ---------- remote jobs (src/jobrun.rs): the Settings row; the strip is Notices.jobNotice ---------- var jobTitle = Notices.jobTitle; function sampleJob(kind, now) { var j = { allowed: true, key_fingerprint: '8f186e37', url_set: true, checked_at: now - 30, error: '', queued: 0, account: '', active: false, id: '', kind: '', title: '', stage: '', message: '', started_at: 0, run_id: '', results: [], last: {}, last_results: [], history: [] }; @@ -1378,10 +1616,18 @@ if (typeof document !== 'undefined') (function () { else if (kind === 'failed') { j.last = { id: 'job-2026-10-05-01', kind: 'build', title: 'build', status: 'failed', started_at: now - 2000, finished_at: now - 900, exit: 1, run_id: 'r0', uploaded: false, summary: 'build exited with code 1' }; j.last_results = ['STAGE build node', 'BUILD FAILED: error[E0425]: cannot find value `foo` in this scope (src/engine.rs:2082)']; j.history = [j.last]; } return j; } + // ?tune=running|tuned|measured : a sample tune state on the first card (screenshots, no engine) + function sampleTune(s, kind) { + var c = s.mining.cards[0]; if (!c) return; + if (kind === 'running') { c.state = 'tuning'; c.sweep_state = 'running'; c.tune_step = 7; c.tune_steps = 9; c.tune_eta_s = 230; c.tune_plan = 'full'; c.sweep_note = 'tuning: holding 2472 MHz · 100% · 41 s (step 7 of 9)'; if (!(c.power_w > 0)) c.power_w = 219.6; if (!(c.hash_now > 0)) c.hash_now = 123.4; } + if (kind === 'tuned') { c.tune_line = 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)'; c.tune_source = 'full'; c.tune_control = true; c.tune_clock_mhz = 2470; c.sweep_pct = 100; c.sweep_at = s.now - 7200; c.sweep_watts = 290; } + if (kind === 'measured') { c.tune_line = 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W)'; c.tune_source = 'baseline'; c.tune_control = false; c.sweep_at = s.now - 600; c.sweep_note = 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed'; } + } function render(s) { state = s; stateAt = performance.now(); - if (forcedUpdate) { s.update = sampleUpdate(forcedUpdate); if (forcedUpdate === 'updated') { s.version = '0.3.7'; s.uptime_s = 20; } } + if (forcedUpdate) { s.update = sampleUpdate(forcedUpdate); if (forcedUpdate === 'updated') { s.version = '0.3.14'; s.uptime_s = 20; } } if (forcedJob) s.jobs = sampleJob(forcedJob, s.now || 0); + if (params.get('tune')) sampleTune(s, params.get('tune')); if (params.get('uptime')) s.uptime_s = parseInt(params.get('uptime'), 10) || 0; // the card's first-minute rule, for screenshots renderPill(s); renderClock(s); @@ -1415,9 +1661,9 @@ if (typeof document !== 'undefined') (function () { if (failures >= 4) { $('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'stopped' : 'engine away'; if (phase === 'dashboard') { - setText('d-node', 'no answer'); setText('d-node-sub', 'the engine is not answering; the window reconnects by itself'); $('n-state-cell').classList.add('bad'); + setText('node-line-text', 'Node: no answer'); setText('node-sub', 'the engine is not answering; the window reconnects by itself'); $('node-card').classList.add('bad'); setText('btn-toggle-sub', 'the engine is not answering'); $('btn-toggle').disabled = true; - setText('d-hash-sub', 'no answer from the engine'); + setText('t-hash-sub', 'no answer from the engine'); } } }); diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 7b423fdf..da1cfcee 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -4,13 +4,14 @@ Igneum Miner - + - +