Merge remote-tracking branch 'origin/app-ui' into release-0.3.5
# Conflicts: # app/igneum-app/ui/app.css
This commit is contained in:
commit
e24516934a
6 changed files with 734 additions and 188 deletions
|
|
@ -1,5 +1,6 @@
|
|||
/* Igneum Miner dashboard. 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. */
|
||||
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), used by every screen. */
|
||||
@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')}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexSans-400.woff2) format('woff2')}
|
||||
|
|
@ -9,53 +10,66 @@
|
|||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/Unbounded-700.woff2) format('woff2')}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(fonts/Unbounded-900.woff2) format('woff2')}
|
||||
|
||||
:root{--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E;
|
||||
--gutter:28px;--card-pad:22px;--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:20px;--gap-tile:12px;
|
||||
--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:52px}
|
||||
: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;
|
||||
/* 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;
|
||||
/* 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);
|
||||
--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:52px;--drawer-h:260px}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none}
|
||||
.mono,code,pre{font-family:var(--mono)}
|
||||
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}
|
||||
.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:52px;letter-spacing:-.01em}
|
||||
h2{font-weight:700;font-size:32px}
|
||||
h3{font-weight:700;font-size:16px}
|
||||
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)}
|
||||
p{margin:0}
|
||||
a{color:inherit}
|
||||
button{font:inherit;color:inherit}
|
||||
.eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:8px}
|
||||
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.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:8px;min-height:44px;padding:10px 20px;border-radius:10px;font-weight:600;font-size:15px;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;white-space:nowrap}
|
||||
.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:hover{transform:translateY(-1px);border-color:var(--ash)}
|
||||
.btn:active{transform:none}
|
||||
.btn:disabled{opacity:.4;cursor:default;transform:none}
|
||||
.btn.primary{background:var(--ember);color:var(--ember-ink);border-color:var(--ember)}
|
||||
.btn.primary:hover{background:#FF6A2B;border-color:#FF6A2B}
|
||||
.btn.big{min-height:52px;padding:12px 28px;font-size:16px}
|
||||
.btn.small{min-height:34px;padding:6px 14px;font-size:13px;border-radius:8px}
|
||||
.btn.tiny{min-height:26px;padding:2px 10px;font-size:12px;border-radius:7px;font-family:var(--mono);font-weight:500}
|
||||
.btn.primary:hover{background:var(--ember-hi);border-color:var(--ember-hi)}
|
||||
.btn.big{min-height:52px;padding:12px 28px;font-size:var(--t-xl)}
|
||||
.btn.small{min-height:34px;padding:6px 14px;font-size:var(--t-base);border-radius:8px}
|
||||
.btn.tiny{min-height:26px;padding:2px 10px;font-size:var(--t-sm);border-radius:7px;font-family:var(--mono);font-weight:500}
|
||||
.btn.ghost{border-color:transparent;color:var(--ink-2)}
|
||||
.btn.ghost:hover{border-color:var(--line-2);color:var(--bone)}
|
||||
.btn.ghost.on{color:var(--molten);border-color:var(--molten-40);background:var(--molten-10)}
|
||||
.btn.danger:hover{color:var(--ember);border-color:var(--ember)}
|
||||
.btn .chev{transition:transform .2s ease;color:var(--ash)}
|
||||
.btn.on .chev{transform:rotate(180deg);color:var(--molten)}
|
||||
.icon-btn{width:38px;height:38px;border-radius:10px;border:1px solid var(--line-2);background:transparent;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:var(--ink-2);font-size:20px;line-height:1}
|
||||
.icon-btn:hover{color:var(--bone);border-color:var(--ash)}
|
||||
:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
|
||||
@media (prefers-reduced-motion:reduce){.btn,.btn .chev{transition:none}}
|
||||
|
||||
/* top bar */
|
||||
.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;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: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 button,.top .pill{-webkit-app-region:no-drag}
|
||||
body.mac .top{padding-left:92px}
|
||||
.brand{display:flex;align-items:center;gap:10px}
|
||||
.brand{display:flex;align-items:center;gap:10px;min-width:0}
|
||||
.brand .word{font-family:var(--head);font-weight:900;font-size:20px;letter-spacing:.06em}
|
||||
.brand .miner{font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--ash);margin-left:4px;padding-top:3px}
|
||||
.top-right{display:flex;align-items:center;gap:12px}
|
||||
.pill{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12px;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)}
|
||||
.pill.on{color:var(--molten);border-color:rgba(255,179,92,.35)}
|
||||
.brand .miner{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.22em;color:var(--ash);margin-left:var(--s-1);padding-top:3px}
|
||||
.top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto}
|
||||
.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}
|
||||
|
|
@ -64,116 +78,119 @@ body.mac .top{padding-left:92px}
|
|||
@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}}
|
||||
|
||||
/* update banner */
|
||||
.banner{position:fixed;top:var(--top);left:0;right:0;z-index:19;display:flex;align-items:center;justify-content:center;gap:14px;padding:10px var(--gutter);background:rgba(242,84,27,.12);border-bottom:1px solid rgba(242,84,27,.4);font-size:14px}
|
||||
|
||||
/* banners under the top bar: clock, update, job */
|
||||
.banner{position:fixed;top:var(--top);left:0;right:0;z-index:19;display:flex;align-items:center;justify-content:center;gap:14px;padding:10px var(--gutter);background:var(--ember-12);border-bottom:1px solid var(--ember-40);font-size:var(--t-md);line-height:1.4}
|
||||
.banner.clock{background:rgba(242,84,27,.14);border-bottom-color:rgba(242,84,27,.5);flex-wrap:wrap}
|
||||
.banner.clock.warn{background:rgba(255,179,92,.1);border-bottom-color:rgba(255,179,92,.4)}
|
||||
.banner .hint{font-size:11px;color:var(--ash);flex-basis:100%;text-align:center}
|
||||
.banner.update{flex-wrap:wrap;row-gap:8px}
|
||||
.banner.clock.warn{background:var(--molten-10);border-bottom-color:var(--molten-40)}
|
||||
.banner .hint{font-size:var(--t-xs);color:var(--ash);flex-basis:100%;text-align:center}
|
||||
.banner.update{flex-wrap:wrap;row-gap:var(--s-2)}
|
||||
.banner.update.urgent{background:rgba(242,84,27,.55);border-bottom-color:var(--ember);color:#fff;font-weight:600}
|
||||
.banner .prog{flex-basis:100%;height:3px;background:rgba(255,255,255,.12);border-radius:2px;overflow:hidden;margin-top:-2px}
|
||||
.banner .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear}
|
||||
/* remote job strip (src/jobrun.rs): running, then the outcome */
|
||||
.banner.job{flex-wrap:wrap;row-gap:8px;background:rgba(255,179,92,.1);border-bottom-color:rgba(255,179,92,.4)}
|
||||
.banner.job{flex-wrap:wrap;row-gap:var(--s-2);background:var(--molten-10);border-bottom-color:var(--molten-40)}
|
||||
.banner.job.failed{background:rgba(242,84,27,.14);border-bottom-color:rgba(242,84,27,.5)}
|
||||
.banner.job .job-results{flex-basis:100%;margin:0;font-size:11px;line-height:1.5;color:var(--ink-2);white-space:pre-wrap;word-break:break-word;max-height:120px;overflow:auto}
|
||||
.job-history table{margin-top:8px}
|
||||
.job-history th{text-align:left;font-weight:500;color:var(--ash);font-size:11px;padding:4px 6px 4px 0}
|
||||
.banner.job .job-results{flex-basis:100%;margin:0;font-size:var(--t-xs);line-height:1.5;color:var(--ink-2);white-space:pre-wrap;word-break:break-word;max-height:120px;overflow:auto}
|
||||
.job-history table{margin-top:var(--s-2)}
|
||||
.job-history th{text-align:left;font-weight:500;color:var(--ash);font-size:var(--t-xs);padding:4px 6px 4px 0}
|
||||
.job-history td{padding:5px 6px 5px 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{margin-top:12px;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:8px}
|
||||
.clock-card.warn{border-color:rgba(255,179,92,.4);background:rgba(255,179,92,.06)}
|
||||
.clock-msg{font-size:14px;color:var(--bone);line-height:1.45}
|
||||
.clock-card{margin-top:var(--s-3);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)}
|
||||
.clock-msg{font-size:var(--t-md);color:var(--bone);line-height:1.45}
|
||||
.clock-card .note{margin-top:0}
|
||||
|
||||
/* screens */
|
||||
main{position:absolute;top:var(--top);bottom:0;left:0;right:0;overflow:auto;padding:0 var(--gutter)}
|
||||
main{position:absolute;top:var(--top);bottom:0;left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain}
|
||||
body.has-bottom main{bottom:var(--bottom)}
|
||||
body.drawer-open main{bottom:calc(var(--bottom) + 260px)}
|
||||
body.drawer-open main{bottom:calc(var(--bottom) + var(--drawer-h))}
|
||||
.screen{display:none;max-width:1080px;margin:0 auto;animation:rise .45s ease}
|
||||
body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-cards,body[data-phase="address"] #screen-address,body[data-phase="dashboard"] #screen-dashboard{display:block}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
||||
@media (prefers-reduced-motion:reduce){.screen{animation:none}}
|
||||
|
||||
/* welcome */
|
||||
.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:18px;padding:40px 0 48px}
|
||||
.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))}
|
||||
@keyframes breathe{0%,100%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}}
|
||||
.lead{font-size:18px;color:var(--ink-2);max-width:54ch}
|
||||
@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}
|
||||
.tile .k{font-family:var(--mono);font-size:11px;letter-spacing:.14em;color:var(--ember)}
|
||||
.tile .t{font-family:var(--head);font-weight:700;font-size:15px;line-height:1.25}
|
||||
.tile .s{font-size:13px;color:var(--ash);line-height:1.45}
|
||||
.cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;justify-content:center;margin-top:10px}
|
||||
.seedline{font-size:12px;color:var(--ash);letter-spacing:.04em}
|
||||
.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}
|
||||
.tile .h{font-family:var(--head);font-weight:700;font-size:var(--t-h3);margin-bottom:var(--s-2)}
|
||||
.cta{display:flex;flex-wrap:wrap;gap:var(--s-3);align-items:center;justify-content:center;margin-top:10px}
|
||||
.seedline{font-size:var(--t-sm);color:var(--ash);letter-spacing:.04em}
|
||||
|
||||
/* steps */
|
||||
.step{max-width:720px;margin:0 auto;padding:56px 0 48px;display:flex;flex-direction:column;gap:16px}
|
||||
.step .sub{font-size:16px;color:var(--ink-2);max-width:60ch}
|
||||
.step .cta{justify-content:flex-start;margin-top:8px}
|
||||
.note{font-size:13px;color:var(--ash);line-height:1.5}
|
||||
.note.small{font-size:12px;word-break:break-all}
|
||||
.cards{display:flex;flex-direction:column;gap:12px;margin-top:8px}
|
||||
.step{max-width:720px;margin:0 auto;padding:56px 0 48px;display:flex;flex-direction:column;gap:var(--s-4)}
|
||||
.step .sub{font-size:var(--t-xl);color:var(--ink-2);max-width:60ch}
|
||||
.step .cta{justify-content:flex-start;margin-top:var(--s-2)}
|
||||
.note{font-size:var(--t-base);color:var(--ash);line-height:1.5}
|
||||
.note.small{font-size:var(--t-sm);word-break:break-all}
|
||||
.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}
|
||||
.card.detecting .t{font-family:var(--head);font-weight:700;font-size:16px}
|
||||
.card.detecting .s{font-size:12px;color:var(--ash);margin-top:4px}
|
||||
.card.detecting .t{font-family:var(--head);font-weight:700;font-size:var(--t-xl)}
|
||||
.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)}}
|
||||
.gpu{display:flex;align-items:center;gap:18px}
|
||||
.gpu .badge{width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;letter-spacing:.08em;flex:0 0 52px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)}
|
||||
.gpu .badge{width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 52px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)}
|
||||
.gpu .badge.apple{color:var(--bone)}
|
||||
.gpu .badge.nvidia{color:#8BE37A}
|
||||
.gpu .badge.nvidia{color:var(--nvidia)}
|
||||
.gpu .badge.amd{color:var(--ember)}
|
||||
.gpu .name{font-family:var(--head);font-weight:700;font-size:18px;line-height:1.2}
|
||||
.gpu .meta{font-family:var(--mono);font-size:12px;color:var(--ash);margin-top:5px;display:flex;flex-wrap:wrap;gap:6px 14px}
|
||||
.gpu .name{font-family:var(--head);font-weight:700;font-size:var(--t-2xl);line-height:1.2}
|
||||
.gpu .meta{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);margin-top:5px;display:flex;flex-wrap:wrap;gap:6px 14px}
|
||||
.gpu .meta b{color:var(--ink-2);font-weight:500}
|
||||
.gpu .tick{margin-left:auto;width:36px;height:36px;border-radius:50%;background:var(--ember);display:flex;align-items:center;justify-content:center;flex:0 0 36px}
|
||||
.gpu.off .tick{background:var(--line-2)}
|
||||
.gpu .tick svg path{stroke-dasharray:30;stroke-dashoffset:30;animation:draw .8s .2s ease forwards}
|
||||
@keyframes draw{to{stroke-dashoffset:0}}
|
||||
.gpu .msg{font-size:12px;color:var(--ember);margin-top:4px}
|
||||
.gpu .msg{font-size:var(--t-sm);color:var(--ember);margin-top:var(--s-1)}
|
||||
|
||||
/* GPU rows (first run and settings) */
|
||||
.gpu-row{display:flex;align-items:center;gap:16px;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0}
|
||||
.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}
|
||||
.gpu-row .badge{width:48px;height:48px;border-radius:13px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;letter-spacing:.08em;flex:0 0 48px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)}
|
||||
.gpu-row .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)}
|
||||
.gpu-row .badge.apple{color:var(--bone)}
|
||||
.gpu-row .badge.nvidia{color:#8BE37A}
|
||||
.gpu-row .badge.nvidia{color:var(--nvidia)}
|
||||
.gpu-row .badge.amd{color:var(--ember)}
|
||||
.gpu-row .info{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
|
||||
.gpu-row .name{font-family:var(--head);font-weight:700;font-size:16px;line-height:1.2;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.kind{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;border-radius:999px;padding:2px 8px;border:1px solid var(--line-2);color:var(--ash);font-weight:500}
|
||||
.kind.discrete,.kind.apple{color:var(--molten);border-color:rgba(255,179,92,.4)}
|
||||
.gpu-row .info{flex:1;min-width:180px;display:flex;flex-direction:column;gap:var(--s-1)}
|
||||
.gpu-row .name{font-family:var(--head);font-weight:700;font-size:var(--t-xl);line-height:1.2;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.kind{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;border-radius:999px;padding:2px 8px;border:1px solid var(--line-2);color:var(--ash);font-weight:500;white-space:nowrap}
|
||||
.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:12px;color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 14px}
|
||||
.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:12px;color:var(--ash)}
|
||||
.gpu-row .msg{font-size:12px;color:var(--ember)}
|
||||
.gpu-row .reason{font-size:var(--t-sm);color:var(--ash)}
|
||||
.gpu-row .msg{font-size:var(--t-sm);color:var(--ember)}
|
||||
.ids{display:flex;align-items:center;gap:6px;flex:0 0 auto}
|
||||
.ids .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);margin-right:4px}
|
||||
.ids .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);margin-right:var(--s-1)}
|
||||
.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:13px;user-select:text;-webkit-user-select:text}
|
||||
.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}
|
||||
.gpu-row.off .ids{opacity:.4;pointer-events:none}
|
||||
.gpu-row .switch{padding:0;flex:0 0 auto}
|
||||
.cards.compact .gpu-row{padding:12px 14px;gap:12px;border-radius:14px}
|
||||
.cards.compact .gpu-row{padding:12px 14px;gap:var(--s-3);border-radius:14px}
|
||||
.cards.compact .gpu-row .badge{width:38px;height:38px;flex-basis:38px;border-radius:10px}
|
||||
.cards.compact .gpu-row .name{font-size:14px}
|
||||
.cards.compact .gpu-row .name{font-size:var(--t-md)}
|
||||
.cards.compact .gpu-row .info{flex-basis:calc(100% - 50px);min-width:120px}
|
||||
.cards.compact .gpu-row .power{margin-left:50px}
|
||||
.cards.compact .gpu-row .switch{margin-left:auto}
|
||||
.cards.compact .ids .k{display:none}
|
||||
|
||||
.power{display:flex;align-items:center;gap:8px;flex:0 0 auto}
|
||||
.power{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto}
|
||||
.power .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
|
||||
.power input[type=range]{width:110px;accent-color:var(--ember)}
|
||||
.power .pv{font-size:12px;color:var(--ink-2);min-width:84px}
|
||||
.power .pin{font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line-2);border-radius:6px;padding:1px 6px}
|
||||
.power .pv{font-size:var(--t-sm);color:var(--ink-2);min-width:84px}
|
||||
.power .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}
|
||||
.gpu-tile .msg.sweep{color:var(--ash)}
|
||||
.gpu-tile .msg.sweep b{color:var(--ink-2);font-weight:500}
|
||||
.gpu-tile .msg.sweep.on{color:var(--molten)}
|
||||
|
|
@ -190,22 +207,22 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
/* per-card tiles on the dashboard */
|
||||
.gpu-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--gap-tile)}
|
||||
.gpu-tile{background:var(--obsidian);border:1px solid var(--line);border-radius:var(--tile-r);padding:14px 16px;display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||
.gpu-tile .n{font-family:var(--head);font-weight:700;font-size:14px;line-height:1.25;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.gpu-tile .n{font-family:var(--head);font-weight:700;font-size:var(--t-md);line-height:1.25;display:flex;align-items:center;gap:var(--s-2);flex-wrap:wrap}
|
||||
.gpu-tile .h{font-family:var(--head);font-weight:700;font-size:24px;color:var(--ember);line-height:1.1;display:flex;align-items:baseline;gap:6px;font-variant-numeric:tabular-nums}
|
||||
.gpu-tile .h .unit{font-family:var(--mono);font-size:11px;color:var(--ash);font-weight:500;letter-spacing:.08em}
|
||||
.gpu-tile .h .unit{font-family:var(--mono);font-size:var(--t-xs);color:var(--ash);font-weight:500;letter-spacing:.08em}
|
||||
.gpu-tile.idle .h{color:var(--ash)}
|
||||
.gpu-tile .m{font-family:var(--mono);font-size:12px;color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 12px}
|
||||
.gpu-tile .m{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 12px}
|
||||
.gpu-tile .m b{color:var(--ink-2);font-weight:500}
|
||||
.gpu-tile .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--ash)}
|
||||
.gpu-tile .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash)}
|
||||
.gpu-tile .st.mining{color:var(--molten)}
|
||||
.gpu-tile .st.bad{color:var(--ember)}
|
||||
.gpu-tile .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block}
|
||||
.gpu-tile .msg{font-size:12px;color:var(--ash)}
|
||||
.gpu-off{margin-top:12px;font-size:12px;color:var(--ash)}
|
||||
.gpu-tile .msg{font-size:var(--t-sm);color:var(--ash)}
|
||||
.gpu-off{margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ash)}
|
||||
|
||||
/* address options */
|
||||
.options{display:flex;flex-direction:column;gap:12px;margin-top:6px}
|
||||
.option{display:flex;gap:16px;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}
|
||||
.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 input[type=radio]{position:absolute;opacity:0;width:0;height:0}
|
||||
|
|
@ -213,61 +230,63 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.option.on .radio{border-color:var(--ember)}
|
||||
.option.on .radio::after{content:"";position:absolute;inset:4px;border-radius:50%;background:var(--ember)}
|
||||
.option .body{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}
|
||||
.option .t{font-family:var(--head);font-weight:700;font-size:16px;display:flex;align-items:center;gap:10px}
|
||||
.option .s{font-size:14px;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 rgba(255,179,92,.4);border-radius:999px;padding:2px 8px}
|
||||
.addr-input{width:100%;margin-top:8px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;color:var(--bone);font-size:14px;letter-spacing:.02em;user-select:text;-webkit-user-select:text}
|
||||
.option .t{font-family:var(--head);font-weight:700;font-size:var(--t-xl);display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.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:focus{outline:none;border-color:var(--ember)}
|
||||
.option:not(.on) .addr-input{display:none}
|
||||
.err{font-size:13px;color:var(--ember)}
|
||||
.err{font-size:var(--t-base);color:var(--ember)}
|
||||
|
||||
/* dashboard */
|
||||
#screen-dashboard{padding:22px 0 28px;display:none;flex-direction:column;gap:var(--gap)}
|
||||
body[data-phase="dashboard"] #screen-dashboard{display:flex}
|
||||
.strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap-tile)}
|
||||
.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:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
|
||||
.cell .v{font-family:var(--head);font-weight:700;font-size:26px;line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:baseline;gap:8px}
|
||||
.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:12px;color:var(--ash);font-weight:500;letter-spacing:.08em}
|
||||
.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 .s{font-family:var(--mono);font-size:12px;color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.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)}
|
||||
.grid{display:grid;grid-template-columns:1.35fr .85fr;gap:var(--gap);align-items:start}
|
||||
.col-main,.col-side{display:flex;flex-direction:column;gap:var(--gap);min-width:0}
|
||||
.card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;flex-wrap:wrap}
|
||||
.stats{display:flex;flex-wrap:wrap;gap:12px 16px;font-size:12px;color:var(--ash)}
|
||||
.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:190px;border-radius:12px;background:var(--obsidian)}
|
||||
.legend{display:flex;flex-wrap:wrap;gap:14px 18px;margin-top:12px;font-size:12px;color:var(--ink-2)}
|
||||
.legend span{display:inline-flex;align-items:center;gap:8px}
|
||||
.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}
|
||||
.tbl{overflow-x:auto}
|
||||
table{border-collapse:collapse;width:100%;font-size:13px}
|
||||
table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
||||
th,td{padding:9px 8px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
|
||||
th{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);font-weight:500}
|
||||
td.n,th.n{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--mono)}
|
||||
tr:last-child td{border-bottom:0}
|
||||
td .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--ash)}
|
||||
td .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash)}
|
||||
td .st.mining{color:var(--molten)}
|
||||
td .st.bad{color:var(--ember)}
|
||||
td .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block}
|
||||
td .sub{display:block;font-family:var(--mono);font-size:11px;color:var(--ash);white-space:normal;max-width:280px}
|
||||
.empty{color:var(--ash);font-size:13px;padding:10px 0}
|
||||
td .sub{display:block;font-family:var(--mono);font-size:var(--t-xs);color:var(--ash);white-space:normal;max-width:280px}
|
||||
.empty{color:var(--ash);font-size:var(--t-base);padding:10px 0}
|
||||
.kv{display:flex;flex-direction:column}
|
||||
.kv>div{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:7px 0;border-bottom:1px solid var(--line)}
|
||||
.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:last-child{border-bottom:0}
|
||||
.kv .k{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ash)}
|
||||
.kv .v{font-size:14px;font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.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}
|
||||
.card .note{margin-top:10px}
|
||||
.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:12px;color:var(--ink-2);max-height:300px;overflow:auto}
|
||||
.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);max-height:300px;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>div:last-child{border-bottom:0}
|
||||
.feed .t{color:var(--ash);flex:0 0 auto;font-size:11px}
|
||||
.feed>div>span:first-child{min-width:0}
|
||||
.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.block{color:var(--ember)}
|
||||
|
|
@ -275,61 +294,128 @@ td .sub{display:block;font-family:var(--mono);font-size:11px;color:var(--ash);wh
|
|||
|
||||
/* sheet (the key) */
|
||||
.sheet-wrap,.panel-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%;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px rgba(0,0,0,.6);animation:rise .3s ease}
|
||||
.sheet .sub{font-size:15px;color:var(--ink-2)}
|
||||
.field{display:flex;flex-direction:column;gap:8px;margin-top:6px}
|
||||
.field .k{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
|
||||
.box{display:flex;align-items:center;gap:10px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;font-size:13px;word-break:break-all;user-select:text;-webkit-user-select:text}
|
||||
.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 .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)}
|
||||
.box{display:flex;align-items:center;gap:10px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;font-size:var(--t-base);word-break:break-all;user-select:text;-webkit-user-select:text}
|
||||
.box span{flex:1;min-width:0}
|
||||
.box.key{color:var(--molten)}
|
||||
.check{display:flex;align-items:center;gap:10px;font-size:14px;cursor:pointer;margin-top:4px}
|
||||
.check.small{font-size:12px;color:var(--ash)}
|
||||
.check input{width:18px;height:18px;accent-color:var(--ember)}
|
||||
.check{display:flex;align-items:center;gap:10px;font-size:var(--t-md);cursor:pointer;margin-top:var(--s-1)}
|
||||
.check.small{font-size:var(--t-sm);color:var(--ash);margin-top:0}
|
||||
.check input{width:18px;height:18px;accent-color:var(--ember);margin:0}
|
||||
.check.small input{width:15px;height:15px}
|
||||
.sheet .cta{justify-content:flex-start}
|
||||
|
||||
/* settings panel */
|
||||
.panel-wrap{justify-content:flex-end;padding:0}
|
||||
.panel{width:min(440px,100%);height:100%;background:var(--graphite);border-left:1px solid var(--line-2);display:flex;flex-direction:column;animation:slide .25s ease}
|
||||
@keyframes slide{from{transform:translateX(30px);opacity:0}to{transform:none;opacity:1}}
|
||||
.panel-head{display:flex;justify-content:space-between;align-items:center;padding:18px 22px;border-bottom:1px solid var(--line)}
|
||||
.panel-body{padding:14px 22px 28px;overflow:auto;display:flex;flex-direction:column;gap:18px}
|
||||
.row{display:flex;gap:10px;align-items:center}
|
||||
@media (prefers-reduced-motion:reduce){.panel,.sheet{animation:none}}
|
||||
.panel-head{display:flex;justify-content:space-between;align-items:center;padding:18px 22px;border-bottom:1px solid var(--line);flex:0 0 auto}
|
||||
.panel-body{padding:14px 22px 28px;overflow:auto;display:flex;flex-direction:column;gap:18px;overscroll-behavior:contain}
|
||||
.row{display:flex;gap:10px;align-items:center;min-width:0}
|
||||
.row.between{justify-content:space-between}
|
||||
.row .addr-input{margin-top:0}
|
||||
.num{width:90px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:9px 12px;color:var(--bone);font-size:14px;user-select:text;-webkit-user-select:text}
|
||||
.row .addr-input{margin-top:0;min-width:0}
|
||||
.num{width:90px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:9px 12px;color:var(--bone);font-size:var(--t-md);user-select:text;-webkit-user-select:text}
|
||||
.num:focus{outline:none;border-color:var(--ember)}
|
||||
.switch{display:flex;align-items:center;gap:12px;font-size:14px;cursor:pointer;padding:6px 0}
|
||||
.switch{display:flex;align-items:center;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:6px 0;line-height:1.35}
|
||||
.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 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}
|
||||
|
||||
/* bottom bar */
|
||||
.bottom{position:fixed;left:0;right:0;bottom:0;height:var(--bottom);display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 var(--gutter);background:rgba(12,12,14,.92);border-top:1px solid var(--line);z-index:21}
|
||||
.bottom .left,.bottom .right{display:flex;align-items:center;gap:8px}
|
||||
.bottom .mid{font-size:12px;color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;flex:1}
|
||||
.bottom .right .mono{font-size:12px}
|
||||
.bottom{position:fixed;left:0;right:0;bottom:0;height:var(--bottom);display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);padding:0 var(--gutter);background:rgba(12,12,14,.92);border-top:1px solid var(--line);z-index:21}
|
||||
.bottom .left,.bottom .right{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto}
|
||||
.bottom .mid{font-size:var(--t-sm);color:var(--ash);display:flex;align-items:center;justify-content:center;gap:0;min-width:0;flex:1;overflow:hidden;white-space:nowrap}
|
||||
.bottom .mid .pc{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
||||
.bottom .mid .pc+.pc::before{content:"·";margin:0 7px;color:var(--line-2)}
|
||||
.bottom .mid .nm{color:var(--ink-2);max-width:220px;flex-shrink:1}
|
||||
.bottom .mid .ad{flex-shrink:0}
|
||||
.bottom .right .mono{font-size:var(--t-sm)}
|
||||
@media (max-width:1100px){.bottom .mid .up{display:none}.bottom .mid .nm{max-width:140px}.log-tools .at{display:none}.log-search{flex-basis:120px;max-width:200px}}
|
||||
@media (max-width:960px){.bottom .mid .ad{display:none}}
|
||||
|
||||
/* log drawer */
|
||||
.drawer{position:fixed;left:0;right:0;bottom:var(--bottom);height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:20;transition:height .2s ease;display:flex;flex-direction:column}
|
||||
body.drawer-open .drawer{height:260px}
|
||||
.drawer-head{display:flex;justify-content:space-between;align-items:center;padding:8px var(--gutter);border-bottom:1px solid var(--line);flex:0 0 auto}
|
||||
.chips{display:flex;gap:6px}
|
||||
.chip{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--line);border-radius:999px;padding:4px 10px;background:transparent;color:var(--ash);cursor:pointer}
|
||||
.chip.on{color:var(--molten);border-color:rgba(255,179,92,.4)}
|
||||
.log{margin:0;flex:1;overflow:auto;padding:10px var(--gutter);font-size:12px;line-height:1.55;color:var(--ink-2);white-space:pre-wrap;word-break:break-all;user-select:text;-webkit-user-select:text}
|
||||
.drawer{position:fixed;left:0;right:0;bottom:var(--bottom);height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:20;transition:height .2s ease;display:flex;flex-direction:column;box-shadow:0 -12px 30px rgba(0,0,0,.35)}
|
||||
body.drawer-open .drawer{height:var(--drawer-h)}
|
||||
body.drawer-drag .drawer{transition:none}
|
||||
body.drawer-drag{cursor:row-resize}
|
||||
body.drawer-drag main{pointer-events:none}
|
||||
@media (prefers-reduced-motion:reduce){.drawer{transition:none}}
|
||||
.drawer-grip{height:10px;flex:0 0 10px;cursor:row-resize;display:flex;align-items:center;justify-content:center;touch-action:none}
|
||||
.drawer-grip i{width:44px;height:3px;border-radius:2px;background:var(--line-2);transition:background .15s ease}
|
||||
.drawer-grip:hover i,body.drawer-drag .drawer-grip i{background:var(--ash)}
|
||||
.drawer-head{display:flex;align-items:center;gap:var(--s-3);padding:2px var(--gutter) 8px;border-bottom:1px solid var(--line);flex:0 0 auto;flex-wrap:wrap;row-gap:6px}
|
||||
.chips{display:flex;gap:6px;flex-wrap:wrap}
|
||||
.chip{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--line);border-radius:999px;padding:4px 10px;background:transparent;color:var(--ash);cursor:pointer;white-space:nowrap;line-height:1.3;transition:color .15s ease,border-color .15s ease}
|
||||
.chip:hover{color:var(--ink-2);border-color:var(--line-2)}
|
||||
.chip.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.chip:disabled{opacity:.4;cursor:default}
|
||||
.log-search{display:flex;align-items:center;gap:6px;background:var(--graphite);border:1px solid var(--line);border-radius:999px;padding:3px 10px 3px 10px;color:var(--ash);flex:1 1 160px;min-width:140px;max-width:300px}
|
||||
.log-search:focus-within{border-color:var(--molten-40);color:var(--ink-2)}
|
||||
.log-search input{flex:1;min-width:0;background:transparent;border:0;outline:0;color:var(--bone);font-family:var(--mono);font-size:var(--t-sm);padding:2px 0;user-select:text;-webkit-user-select:text}
|
||||
.log-search input::-webkit-search-cancel-button{-webkit-appearance:none}
|
||||
.log-search input::placeholder{color:var(--ash)}
|
||||
.log-search .count{font-size:var(--t-xs);color:var(--ash);white-space:nowrap;font-variant-numeric:tabular-nums}
|
||||
.log-nav{display:flex;align-items:center;gap:6px}
|
||||
.log-nav input{width:84px;background:var(--graphite);border:1px solid var(--line);border-radius:999px;padding:4px 10px;color:var(--bone);font-size:var(--t-xs);letter-spacing:.06em;outline:0;user-select:text;-webkit-user-select:text;text-align:center}
|
||||
.log-nav input:focus{border-color:var(--molten-40)}
|
||||
.log-nav input::placeholder{color:var(--ash);letter-spacing:.04em}
|
||||
.log-tools{display:flex;align-items:center;gap:var(--s-2);margin-left:auto}
|
||||
.log-tools .at{font-size:var(--t-xs);min-width:64px;text-align:right}
|
||||
.log-ruler{position:relative;height:16px;flex:0 0 16px;background:var(--graphite);border-bottom:1px solid var(--line);cursor:pointer;overflow:hidden}
|
||||
.log-ruler.empty{cursor:default}
|
||||
.log-ruler.empty>*{display:none}
|
||||
.log-ruler .t0,.log-ruler .t1{position:absolute;top:0;z-index:2;font-size:9px;line-height:16px;color:var(--ash);padding:0 6px;margin:0 var(--gutter);pointer-events:none;letter-spacing:.04em;background:var(--graphite);border-radius:0 0 6px 0}
|
||||
.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-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}
|
||||
.log .ln{height:19px;overflow:hidden;text-overflow:ellipsis;white-space:pre}
|
||||
.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:#7FA7C9}
|
||||
.log .src.miner1,.log .src.miner2,.log .src.miner3,.log .src.miner4{color:var(--molten)}
|
||||
.log .src.node{color:var(--node-blue)}
|
||||
.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 .e{color:var(--ember)}
|
||||
.log .src.watch{color:var(--ash)}
|
||||
.log .e,.log .e .src{color:var(--ember)}
|
||||
.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)}
|
||||
|
||||
.toast{position:fixed;left:50%;bottom:calc(var(--bottom) + 16px);transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:13px;z-index:40;box-shadow:0 10px 30px rgba(0,0,0,.5)}
|
||||
.toast{position:fixed;left:50%;bottom:calc(var(--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}
|
||||
body.drawer-open .toast{bottom:calc(var(--bottom) + var(--drawer-h) + 16px)}
|
||||
|
||||
/* narrow windows: the strip folds to two columns, the side column drops under the main one */
|
||||
@media (max-width:860px){
|
||||
.strip{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||
.grid{grid-template-columns:1fr}
|
||||
.three{grid-template-columns:1fr}
|
||||
h1{font-size:40px}
|
||||
}
|
||||
/* short windows (under 700 px): tighter paddings, a lower hero, a smaller canvas, so the bottom bar and the drawer
|
||||
always have room */
|
||||
@media (max-height:700px){
|
||||
:root{--card-pad:18px;--tile-pad:14px 16px;--gap:var(--s-4)}
|
||||
#screen-dashboard{padding:16px 0 20px}
|
||||
#dag{height:150px}
|
||||
.hero{gap:var(--s-3);padding:var(--s-5) 0 var(--s-6)}
|
||||
.coin-wrap{width:104px;height:104px}
|
||||
.coin{width:104px;height:104px}
|
||||
h1{font-size:40px}
|
||||
.lead{font-size:var(--t-xl)}
|
||||
.step{padding:32px 0 32px}
|
||||
.feed{max-height:220px}
|
||||
.drawer-head{padding-bottom:6px}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,22 @@
|
|||
if (params.get('screen')) forced = params.get('screen');
|
||||
var forcedUpdate = params.get('update'); // a sample update state for screenshots: available|downloading|ready|waiting|applying|urgent|manual|error|updated
|
||||
if (params.get('host') === 'mac') document.body.classList.add('mac');
|
||||
// ?debug=1 prints a budget line every 5 s (ms of JS per second in the canvas, the state render and the log) and
|
||||
// exposes the counters as window.__igneumPerf
|
||||
var debug = params.get('debug') === '1';
|
||||
// the hosts hide the window to the menu bar or the tray: nothing draws or polls fast while document.hidden.
|
||||
// ?visible=1 ignores that (screenshots from a hidden pane).
|
||||
var forceVisible = params.get('visible') === '1';
|
||||
function hiddenNow() { return document.hidden && !forceVisible; }
|
||||
var perf = { frames: 0, draw: 0, render: 0, log: 0, poll: 0, polls: 0, since: performance.now() };
|
||||
if (debug) {
|
||||
window.__igneumPerf = perf;
|
||||
setInterval(function () {
|
||||
var s = (performance.now() - perf.since) / 1000;
|
||||
console.log('[perf] ' + (hiddenNow() ? 'hidden' : 'visible') + ' ' + (perf.frames / s).toFixed(1) + ' fps · js ms/s: canvas ' + (perf.draw / s).toFixed(2) + ' render ' + (perf.render / s).toFixed(2) + ' log ' + (perf.log / s).toFixed(2) + ' poll ' + (perf.poll / s).toFixed(2) + ' · ' + (perf.polls / s).toFixed(2) + ' polls/s · total ' + ((perf.draw + perf.render + perf.log + perf.poll) / s / 10).toFixed(2) + '% of one core');
|
||||
perf.frames = 0; perf.draw = 0; perf.render = 0; perf.log = 0; perf.poll = 0; perf.polls = 0; perf.since = performance.now();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
function api(path, body) {
|
||||
|
|
@ -26,11 +42,12 @@
|
|||
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'); }
|
||||
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) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { toast('Copied'); }, function () { fallbackCopy(text); }); }
|
||||
else fallbackCopy(text);
|
||||
function copyText(text, msg) {
|
||||
msg = msg || 'Copied';
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { toast(msg); }, function () { fallbackCopy(text, msg); }); }
|
||||
else fallbackCopy(text, msg);
|
||||
}
|
||||
function fallbackCopy(text) { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); toast('Copied'); } catch (e) { toast('Select and copy it by hand'); } document.body.removeChild(ta); }
|
||||
function fallbackCopy(text, msg) { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); toast(msg || 'Copied'); } catch (e) { toast('Select and copy it by hand'); } document.body.removeChild(ta); }
|
||||
document.addEventListener('click', function (e) {
|
||||
var b = e.target.closest('[data-copy]'); if (!b) return;
|
||||
copyText($(b.getAttribute('data-copy')).textContent);
|
||||
|
|
@ -44,7 +61,7 @@
|
|||
$('bottom').hidden = !dash;
|
||||
$('btn-settings').hidden = !dash;
|
||||
document.body.classList.toggle('has-bottom', dash);
|
||||
if (!dash) document.body.classList.remove('drawer-open');
|
||||
if (!dash && drawerOpen()) setDrawer(false);
|
||||
if (p === 'cards' && !cardsSeen) { cardsSeen = true; api('api/detect', {}); }
|
||||
api('api/phase', { phase: p });
|
||||
}
|
||||
|
|
@ -158,72 +175,278 @@
|
|||
if (!state) return;
|
||||
api(state.mining.paused ? 'api/resume' : 'api/pause', {});
|
||||
});
|
||||
$('btn-logs').addEventListener('click', function () { document.body.classList.toggle('drawer-open'); if (document.body.classList.contains('drawer-open')) pollLog(); });
|
||||
$('btn-logs').addEventListener('click', function () { setDrawer(!document.body.classList.contains('drawer-open')); });
|
||||
$('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;
|
||||
});
|
||||
|
||||
// ---------- log drawer ----------
|
||||
var logAfter = 0, logFilter = '', logLines = [];
|
||||
document.querySelectorAll('.chip').forEach(function (c) { c.addEventListener('click', function () { document.querySelectorAll('.chip').forEach(function (x) { x.classList.toggle('on', x === c); }); logFilter = c.getAttribute('data-src'); renderLog(); }); });
|
||||
function renderLog() {
|
||||
var el = $('log'), follow = $('log-follow').checked;
|
||||
var lines = logLines.filter(function (l) { return !logFilter || l.src.indexOf(logFilter) === 0; }).slice(-600);
|
||||
el.innerHTML = lines.map(function (l) { return '<span class="src ' + esc(l.src) + '">' + clock(l.t) + ' ' + esc(l.src.padEnd(6)) + '</span> ' + (l.err ? '<span class="e">' : '') + esc(l.text) + (l.err ? '</span>' : ''); }).join('\n');
|
||||
if (follow) el.scrollTop = el.scrollHeight;
|
||||
// logLines: everything fetched this session (newest last, capped at LOG_CAP). view: the lines after the source chip
|
||||
// and the search box. The list is virtualised: rows have a fixed height (RH) and only the rows in the viewport are in
|
||||
// the DOM, so 20,000 lines cost the same as 20. Wrap mode renders the whole view with wrapped lines when it is small
|
||||
// enough. Polling runs only while the drawer is open and the window is visible.
|
||||
var LOG_CAP = 20000, WRAP_MAX = 5000, RH = 19, OVERSCAN = 8;
|
||||
var logAfter = 0, logFilter = '', logQuery = '', logLines = [], view = [], viewDirty = true, wrap = false, follow = true;
|
||||
var logView = $('log-view'), logPre = $('log'), logPad = $('log-pad');
|
||||
var winStart = -1, winEnd = -1, autoScroll = false, scrollRaf = 0, rulerAt = 0, rulerCount = -1, searchT = null, logTimer = null;
|
||||
var ERR_RE = /\[error\]|\berror\b|\bfailed\b|\bpanic\b|exited with code/i, SWAP_RE = /hourly program changed|program changed|seed change|hot swap|new program|exit-on-seed-change/i;
|
||||
function isErr(l) { return l.err || ERR_RE.test(l.text); }
|
||||
function isSwap(l) { return SWAP_RE.test(l.text); }
|
||||
function drawerOpen() { return document.body.classList.contains('drawer-open'); }
|
||||
function setDrawer(open) {
|
||||
document.body.classList.toggle('drawer-open', open);
|
||||
var b = $('btn-logs'); b.classList.toggle('on', open); b.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
$('btn-logs-text').textContent = open ? 'Close logs' : 'Logs';
|
||||
if (open) { applyDrawerHeight(); pollLog(); scheduleLogPoll(); viewDirty = true; renderLog(true); }
|
||||
else if (logTimer) { clearTimeout(logTimer); logTimer = null; }
|
||||
}
|
||||
// height: dragged by the grip, clamped to [140, 70% of the window]; a short window gets a smaller default
|
||||
var drawerH = 0;
|
||||
function defaultDrawerH() { return window.innerHeight < 700 ? 180 : 260; }
|
||||
function applyDrawerHeight() {
|
||||
var max = Math.floor(window.innerHeight * 0.7), min = 140;
|
||||
if (!drawerH) { try { drawerH = parseInt(localStorage.getItem('igneum.drawer-h'), 10) || 0; } catch (e) { } }
|
||||
var h = Math.max(min, Math.min(max, drawerH || defaultDrawerH()));
|
||||
document.body.style.setProperty('--drawer-h', h + 'px');
|
||||
if (drawerOpen()) renderLog(true);
|
||||
}
|
||||
(function () {
|
||||
var grip = $('drawer-grip'), startY = 0, startH = 0;
|
||||
grip.addEventListener('pointerdown', function (e) {
|
||||
startY = e.clientY; startH = parseInt(getComputedStyle(document.body).getPropertyValue('--drawer-h'), 10) || defaultDrawerH();
|
||||
grip.setPointerCapture(e.pointerId); document.body.classList.add('drawer-drag'); e.preventDefault();
|
||||
});
|
||||
grip.addEventListener('pointermove', function (e) {
|
||||
if (!document.body.classList.contains('drawer-drag')) return;
|
||||
drawerH = startH + (startY - e.clientY); applyDrawerHeight();
|
||||
});
|
||||
function end(e) { if (!document.body.classList.contains('drawer-drag')) return; document.body.classList.remove('drawer-drag'); try { localStorage.setItem('igneum.drawer-h', String(parseInt(getComputedStyle(document.body).getPropertyValue('--drawer-h'), 10))); } catch (x) { } }
|
||||
grip.addEventListener('pointerup', end); grip.addEventListener('pointercancel', end);
|
||||
grip.addEventListener('dblclick', function () { drawerH = defaultDrawerH(); applyDrawerHeight(); try { localStorage.removeItem('igneum.drawer-h'); } catch (x) { } });
|
||||
})();
|
||||
window.addEventListener('resize', function () { if (drawerOpen()) applyDrawerHeight(); });
|
||||
document.querySelectorAll('#log-chips .chip').forEach(function (c) { c.addEventListener('click', function () { document.querySelectorAll('#log-chips .chip').forEach(function (x) { x.classList.toggle('on', x === c); }); logFilter = c.getAttribute('data-src'); viewDirty = true; renderLog(true); }); });
|
||||
$('log-search').addEventListener('input', function () { clearTimeout(searchT); var v = this.value; searchT = setTimeout(function () { logQuery = v.trim().toLowerCase(); viewDirty = true; renderLog(true); }, 120); });
|
||||
$('log-search').addEventListener('keydown', function (e) { if (e.key === 'Escape') { this.value = ''; logQuery = ''; viewDirty = true; renderLog(true); } });
|
||||
$('log-wrap').addEventListener('click', function () { wrap = !wrap; this.classList.toggle('on', wrap); logView.classList.toggle('wrap', wrap); renderLog(true); });
|
||||
$('log-follow').addEventListener('change', function () { setFollow(this.checked); if (follow) renderLog(true); });
|
||||
$('log-jump').addEventListener('click', function () { setFollow(true); renderLog(true); });
|
||||
$('log-copy').addEventListener('click', function () {
|
||||
var r = visibleRange(), rows = view.slice(r[0], r[1]);
|
||||
if (!rows.length) { toast('Nothing to copy'); return; }
|
||||
copyText(rows.map(function (l) { return clock(l.t) + ' ' + l.src.padEnd(6) + ' ' + l.text; }).join('\n'), 'Copied ' + rows.length + ' line' + (rows.length === 1 ? '' : 's'));
|
||||
});
|
||||
$('log-last-error').addEventListener('click', function () { jumpToLast(isErr, 'No error in the log'); });
|
||||
$('log-last-swap').addEventListener('click', function () { jumpToLast(isSwap, 'No program swap in the log yet'); });
|
||||
$('log-time').addEventListener('keydown', function (e) { if (e.key === 'Enter') { jumpToTime(this.value); } });
|
||||
$('log-time').addEventListener('change', function () { jumpToTime(this.value); });
|
||||
$('log-ruler').addEventListener('click', function (e) {
|
||||
if (!view.length) return;
|
||||
var r = this.getBoundingClientRect(), f = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
|
||||
var t0 = view[0].t, t1 = view[view.length - 1].t;
|
||||
jumpToIndex(lowerBound(view, t0 + f * (t1 - t0)));
|
||||
});
|
||||
function setFollow(on) { follow = on; $('log-follow').checked = on; $('log-jump').hidden = on; }
|
||||
function lowerBound(arr, t) { var lo = 0, hi = arr.length; while (lo < hi) { var mid = (lo + hi) >> 1; if (arr[mid].t < t) lo = mid + 1; else hi = mid; } return lo; }
|
||||
function jumpToTime(v) {
|
||||
var m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec((v || '').trim());
|
||||
if (!m) { toast('Type a time as HH:MM:SS'); return; }
|
||||
if (!view.length) { toast('Nothing in the log'); return; }
|
||||
var last = new Date(view[view.length - 1].t * 1000), d = new Date(last.getTime());
|
||||
d.setHours(+m[1], +m[2], +(m[3] || 0), 0);
|
||||
if (d.getTime() > last.getTime() + 1000) d.setDate(d.getDate() - 1); // a time later than the newest line means yesterday
|
||||
var target = d.getTime() / 1000;
|
||||
if (target < view[0].t - 1) { toast('The log starts at ' + clock(view[0].t)); return; }
|
||||
jumpToIndex(lowerBound(view, target));
|
||||
}
|
||||
function jumpToLast(test, missing) {
|
||||
for (var i = view.length - 1; i >= 0; i--) if (test(view[i])) { jumpToIndex(i); return; }
|
||||
toast(missing);
|
||||
}
|
||||
var hitSeq = 0, hitT = 0;
|
||||
function jumpToIndex(i) {
|
||||
if (!view.length) return;
|
||||
i = Math.max(0, Math.min(view.length - 1, i));
|
||||
setFollow(false);
|
||||
hitSeq = view[i].seq; hitT = performance.now();
|
||||
autoScroll = true;
|
||||
logView.scrollTop = Math.max(0, rowTop(i) - Math.floor(logView.clientHeight / 3));
|
||||
renderLog(true);
|
||||
setTimeout(function () { hitSeq = 0; renderLog(true); }, 1600);
|
||||
}
|
||||
function rowTop(i) {
|
||||
if (!wrap) return i * RH;
|
||||
var kids = logPre.children; if (i < kids.length) return kids[i].offsetTop; return logPre.scrollHeight;
|
||||
}
|
||||
function visibleRange() {
|
||||
if (!view.length) return [0, 0];
|
||||
if (wrap) { // find by offsetTop (the rows are all in the DOM)
|
||||
var kids = logPre.children, top = logView.scrollTop, bottom = top + logView.clientHeight, a = -1, b = kids.length;
|
||||
for (var k = 0; k < kids.length; k++) { var y = kids[k].offsetTop; if (a < 0 && y + kids[k].offsetHeight > top) a = k; if (y >= bottom) { b = k; break; } }
|
||||
return [Math.max(0, a), b];
|
||||
}
|
||||
var s = Math.floor(logView.scrollTop / RH), n = Math.ceil(logView.clientHeight / RH) + 1;
|
||||
return [Math.max(0, s), Math.min(view.length, s + n)];
|
||||
}
|
||||
function rebuildView() {
|
||||
var q = logQuery;
|
||||
view = logLines.filter(function (l) { return (!logFilter || l.src.indexOf(logFilter) === 0) && (!q || l.text.toLowerCase().indexOf(q) >= 0 || l.src.indexOf(q) >= 0); });
|
||||
viewDirty = false; winStart = winEnd = -1;
|
||||
}
|
||||
function appendView(lines) {
|
||||
var q = logQuery;
|
||||
for (var i = 0; i < lines.length; i++) { var l = lines[i]; if ((!logFilter || l.src.indexOf(logFilter) === 0) && (!q || l.text.toLowerCase().indexOf(q) >= 0 || l.src.indexOf(q) >= 0)) view.push(l); }
|
||||
}
|
||||
function rowHtml(l) {
|
||||
var text = esc(l.text), q = logQuery;
|
||||
if (q) { // highlight every match (case-insensitive, on the escaped text; the query itself is escaped too)
|
||||
var eq = esc(q), lower = text.toLowerCase(), out = '', at = 0, idx;
|
||||
while ((idx = lower.indexOf(eq, at)) >= 0) { out += text.slice(at, idx) + '<mark>' + text.slice(idx, idx + eq.length) + '</mark>'; at = idx + eq.length; }
|
||||
text = out + text.slice(at);
|
||||
}
|
||||
return '<div class="ln' + (isErr(l) ? ' e' : '') + (hitSeq && l.seq === hitSeq ? ' hit' : '') + '" title="' + clock(l.t) + '"><span class="src ' + esc(l.src) + '">' + clock(l.t) + ' ' + esc(l.src.padEnd(6)) + '</span> ' + text + '</div>';
|
||||
}
|
||||
function renderLog(force) {
|
||||
if (!drawerOpen()) return;
|
||||
var a = performance.now();
|
||||
if (viewDirty) rebuildView();
|
||||
var n = view.length;
|
||||
$('log-empty').hidden = n > 0;
|
||||
$('log-empty').textContent = logLines.length ? 'No line matches.' : 'Nothing logged yet. Lines appear as the node and the miner write them.';
|
||||
$('log-count').textContent = (logQuery || logFilter) ? (n + ' of ' + logLines.length) : (n ? String(n) : '');
|
||||
var canWrap = n <= WRAP_MAX; $('log-wrap').disabled = !canWrap; $('log-wrap').title = canWrap ? 'Wrap long lines' : 'Wrap is off above 5,000 lines; filter or search first';
|
||||
if (wrap && canWrap) {
|
||||
logPad.style.height = ''; logPre.style.transform = '';
|
||||
if (force || winStart !== 0 || winEnd !== n) { logPre.innerHTML = view.map(rowHtml).join(''); winStart = 0; winEnd = n; }
|
||||
} else {
|
||||
if (wrap && !canWrap) { wrap = false; $('log-wrap').classList.remove('on'); logView.classList.remove('wrap'); }
|
||||
logPad.style.height = (n * RH) + 'px';
|
||||
if (follow) { autoScroll = true; logView.scrollTop = Math.max(0, n * RH - logView.clientHeight); }
|
||||
else if (logView.scrollTop > n * RH) { autoScroll = true; logView.scrollTop = Math.max(0, n * RH - logView.clientHeight); }
|
||||
var r = visibleRange(), s = Math.max(0, r[0] - OVERSCAN), e = Math.min(n, r[1] + OVERSCAN);
|
||||
if (force || s !== winStart || e !== winEnd) {
|
||||
var h = ''; for (var i = s; i < e; i++) h += rowHtml(view[i]);
|
||||
logPre.innerHTML = h; logPre.style.transform = 'translateY(' + (s * RH) + 'px)'; winStart = s; winEnd = e;
|
||||
}
|
||||
}
|
||||
if (follow && wrap) { autoScroll = true; logView.scrollTop = logView.scrollHeight; }
|
||||
updateAt(); updateRuler(force);
|
||||
perf.log += performance.now() - a;
|
||||
}
|
||||
function updateAt() {
|
||||
var r = visibleRange();
|
||||
$('log-at').textContent = view.length ? clock(view[Math.min(view.length - 1, r[0])].t) : '';
|
||||
}
|
||||
// the ruler: the span of the view, error (ember) and swap (molten) marks, and the window in view; rebuilt when the
|
||||
// count changes, at most every 2 s, from a sample of at most 4,000 lines
|
||||
function updateRuler(force) {
|
||||
var ruler = $('log-ruler'), n = view.length;
|
||||
if (!n) { ruler.classList.add('empty'); return; }
|
||||
ruler.classList.remove('empty');
|
||||
var t0 = view[0].t, t1 = view[n - 1].t, span = Math.max(1, t1 - t0);
|
||||
var r = visibleRange(), win = $('log-ruler-win'), i0 = Math.min(n - 1, r[0]), i1 = Math.max(i0, Math.min(n - 1, r[1] - 1));
|
||||
win.style.left = ((view[i0].t - t0) / span * 100) + '%';
|
||||
win.style.width = Math.max(0.5, (view[i1].t - view[i0].t) / span * 100) + '%';
|
||||
var now = performance.now();
|
||||
if (n === rulerCount && now - rulerAt < 2000) return;
|
||||
rulerAt = now; rulerCount = n;
|
||||
$('log-ruler-0').textContent = clock(t0); $('log-ruler-1').textContent = clock(t1);
|
||||
ruler.querySelectorAll('.mk').forEach(function (m) { m.remove(); });
|
||||
var step = Math.max(1, Math.floor(n / 4000)), h = '';
|
||||
for (var i = 0; i < n; i += step) { var l = view[i]; var sw = isSwap(l); if (sw || isErr(l)) h += '<i class="mk ' + (sw ? 'swap' : 'err') + '" style="left:' + ((l.t - t0) / span * 100).toFixed(2) + '%" title="' + clock(l.t) + (sw ? ' program swap' : ' error') + '"></i>'; }
|
||||
win.insertAdjacentHTML('beforebegin', h);
|
||||
}
|
||||
logView.addEventListener('scroll', function () {
|
||||
if (autoScroll) { autoScroll = false; return; }
|
||||
var atEnd = logView.scrollTop + logView.clientHeight >= logView.scrollHeight - RH;
|
||||
if (atEnd !== follow) setFollow(atEnd);
|
||||
if (!scrollRaf) scrollRaf = requestAnimationFrame(function () { scrollRaf = 0; renderLog(false); });
|
||||
});
|
||||
function pollLog() {
|
||||
if (!document.body.classList.contains('drawer-open')) return;
|
||||
api('api/log?after=' + logAfter + '&limit=400').then(function (r) {
|
||||
if (r.lines && r.lines.length) { logLines = logLines.concat(r.lines).slice(-3000); logAfter = r.lines[r.lines.length - 1].seq; renderLog(); }
|
||||
if (!drawerOpen() || hiddenNow()) return;
|
||||
api('api/log?after=' + logAfter + '&limit=' + (logAfter ? 400 : 5000)).then(function (r) {
|
||||
perf.polls++;
|
||||
if (r.lines && r.lines.length) {
|
||||
logLines = logLines.concat(r.lines); logAfter = r.lines[r.lines.length - 1].seq;
|
||||
if (logLines.length > LOG_CAP) { logLines = logLines.slice(-LOG_CAP); viewDirty = true; } else if (!viewDirty) appendView(r.lines);
|
||||
var a = performance.now(); renderLog(false); perf.poll += performance.now() - a;
|
||||
}
|
||||
}).catch(function () { });
|
||||
}
|
||||
setInterval(pollLog, 1500);
|
||||
function scheduleLogPoll() { clearTimeout(logTimer); logTimer = setTimeout(function () { pollLog(); if (drawerOpen()) scheduleLogPoll(); }, 1500); }
|
||||
|
||||
// ---------- the blocks strip (your blocks over the last 10 minutes) ----------
|
||||
var c = $('dag'), x = c.getContext('2d'), W = 0, H = 0, seen = {}, born = {};
|
||||
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); }
|
||||
// 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;
|
||||
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;
|
||||
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); }
|
||||
}
|
||||
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();
|
||||
requestDraw();
|
||||
}
|
||||
window.addEventListener('resize', function () { size(); });
|
||||
function rrect(px, py, w, h, r) { x.beginPath(); x.moveTo(px + r, py); x.lineTo(px + w - r, py); x.quadraticCurveTo(px + w, py, px + w, py + r); x.lineTo(px + w, py + h - r); x.quadraticCurveTo(px + w, py + h, px + w - r, py + h); x.lineTo(px + r, py + h); x.quadraticCurveTo(px, py + h, px, py + h - r); x.lineTo(px, py + r); x.quadraticCurveTo(px, py, px + r, py); x.closePath(); }
|
||||
// returns true while something is animating (a flash), so the caller keeps the fast rate
|
||||
function draw(now) {
|
||||
if (!W || !H) size();
|
||||
if (!W) return;
|
||||
if (!W) return false;
|
||||
x.clearRect(0, 0, W, H);
|
||||
var span = 600, S = 18, pad = 10, right = W - 26, left = pad;
|
||||
var pxs = (right - left) / span;
|
||||
x.drawImage(grid, 0, 0, W, H);
|
||||
var right = W - RIGHT_PAD, pxs = (right - PAD) / SPAN;
|
||||
var nowU = state ? (state.now + (performance.now() - stateAt) / 1000) : Date.now() / 1000;
|
||||
// minute lines
|
||||
x.font = '500 10px IBM Plex Mono, monospace'; x.textAlign = 'center';
|
||||
for (var m = 0; m <= 10; m++) {
|
||||
var t = nowU - m * 60, px = right - (nowU - t) * pxs;
|
||||
x.strokeStyle = m === 0 ? 'rgba(255,179,92,0.5)' : 'rgba(42,42,48,0.9)'; x.lineWidth = 1; x.setLineDash(m === 0 ? [] : [3, 6]);
|
||||
x.beginPath(); x.moveTo(px, 14); x.lineTo(px, H - 18); x.stroke(); x.setLineDash([]);
|
||||
if (px > 30) { x.fillStyle = 'rgba(154,154,158,0.8)'; x.fillText(m === 0 ? 'now' : '-' + m + ' min', px, H - 5); }
|
||||
}
|
||||
var found = state ? state.mining.found : [];
|
||||
var rail = H * 0.5;
|
||||
x.strokeStyle = 'rgba(58,58,66,0.9)'; x.lineWidth = 1; x.beginPath(); x.moveTo(left, rail); x.lineTo(right, rail); x.stroke();
|
||||
var shown = 0;
|
||||
found.forEach(function (t, i) {
|
||||
var age = nowU - t; if (age < 0 || age > span) return; shown++;
|
||||
var rail = H * 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] = now;
|
||||
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;
|
||||
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(); }
|
||||
});
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
return animating && !reduce;
|
||||
}
|
||||
var stateAt = performance.now();
|
||||
function frame(now) { if (phase === 'dashboard') draw(now); if (!reduce) requestAnimationFrame(frame); else setTimeout(function () { frame(performance.now()); }, 1000); }
|
||||
requestAnimationFrame(frame);
|
||||
var stateAt = performance.now(), drawTimer = null, drawRaf = 0;
|
||||
function frame(now) {
|
||||
drawRaf = 0; clearTimeout(drawTimer); drawTimer = null;
|
||||
if (hiddenNow()) return;
|
||||
var fast = false;
|
||||
if (phase === 'dashboard') { var a = performance.now(); fast = draw(now); perf.draw += performance.now() - a; perf.frames++; }
|
||||
if (fast) drawRaf = requestAnimationFrame(frame);
|
||||
else drawTimer = setTimeout(function () { drawTimer = null; drawRaf = requestAnimationFrame(frame); }, reduce ? 1000 : 500);
|
||||
}
|
||||
function requestDraw() { if (hiddenNow() || drawRaf) return; clearTimeout(drawTimer); drawTimer = null; drawRaf = requestAnimationFrame(frame); }
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (hiddenNow()) { clearTimeout(drawTimer); drawTimer = null; if (drawRaf) { cancelAnimationFrame(drawRaf); drawRaf = 0; } clearTimeout(logTimer); logTimer = null; }
|
||||
else { poll(); requestDraw(); if (drawerOpen()) { pollLog(); scheduleLogPoll(); } }
|
||||
});
|
||||
requestDraw();
|
||||
|
||||
// ---------- render ----------
|
||||
// ---------- GPU rows: one per card, with a switch and an identities stepper ----------
|
||||
|
|
@ -333,7 +556,20 @@
|
|||
var b = e.target.closest('[data-power-retry]'); if (!b) return;
|
||||
api('api/power/apply', {}).then(function () { toast('Administrator prompt: allow it to set the cap'); });
|
||||
});
|
||||
var lastEventsKey = '';
|
||||
var lastEventsKey = '', lastFoot = '';
|
||||
function skewWord(sk) { sk = Math.round(Math.abs(sk)); 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'; }
|
||||
// sync ETA from the block rate over the last minute of readings: "about 2 h 10 min left"
|
||||
var syncSamples = [];
|
||||
function syncEta(n, now) {
|
||||
if (n.state !== 'syncing' || !(n.headers > n.blocks)) { syncSamples = []; return ''; }
|
||||
if (!syncSamples.length || now - syncSamples[syncSamples.length - 1].t >= 1) syncSamples.push({ t: now, b: n.blocks });
|
||||
while (syncSamples.length > 2 && now - syncSamples[0].t > 90) syncSamples.shift();
|
||||
var a = syncSamples[0], b = syncSamples[syncSamples.length - 1];
|
||||
if (b.t - a.t < 5 || b.b <= a.b) return syncSamples.length > 15 ? 'no progress in the last minute' : 'measuring the sync rate';
|
||||
var rate = (b.b - a.b) / (b.t - a.t), left = (n.headers - n.blocks) / rate;
|
||||
var word = left < 60 ? 'under a minute' : left < 3600 ? 'about ' + Math.round(left / 60) + ' min' : left < 86400 ? 'about ' + Math.floor(left / 3600) + ' h ' + Math.round(left % 3600 / 60) + ' min' : 'over a day';
|
||||
return word + ' left at ' + Math.round(rate) + ' blocks/s';
|
||||
}
|
||||
function renderDashboard(s) {
|
||||
var m = s.mining, n = s.node, p = s.program, f = s.finality;
|
||||
$('d-hash').textContent = m.hash_total >= 100 ? m.hash_total.toFixed(0) : m.hash_total.toFixed(1);
|
||||
|
|
@ -345,18 +581,22 @@
|
|||
$('d-node').textContent = nodeWord;
|
||||
// the live numbers always show, so a stuck label is visible as stuck
|
||||
var live = 'height ' + withCommas(n.blocks) + (n.headers > n.blocks ? ' of ' + withCommas(n.headers) : '') + ' · daa ' + withCommas(n.daa) + ' · ' + n.peers + ' peer' + (n.peers === 1 ? '' : 's');
|
||||
$('d-node-sub').textContent = (s.clock && s.clock.severity === 'block') ? 'clock ' + Math.round(Math.abs(s.clock.skew_s)) + ' s ' + (s.clock.skew_s < 0 ? 'behind' : 'ahead') + ': fix it to mine' : n.state === 'restarting' ? ('restart in ' + n.restart_in_s + ' s · ' + live) : (n.state === 'syncing' && n.message ? n.message + ' · ' : '') + live;
|
||||
$('d-node-sub').title = n.last_reading_age_s >= 0 ? 'last reading ' + Math.round(n.last_reading_age_s) + ' s ago' : '';
|
||||
var eta = syncEta(n, s.now);
|
||||
var syncing = n.state === 'syncing' && n.headers > n.blocks ? (eta || n.message || 'downloading blocks') + ' · ' + withCommas(n.blocks) + ' of ' + withCommas(n.headers) + ' (' + Math.floor(n.blocks / n.headers * 100) + '%)' : '';
|
||||
$('d-node-sub').textContent = (s.clock && s.clock.severity === 'block') ? 'clock ' + skewWord(s.clock.skew_s) + ' ' + (s.clock.skew_s < 0 ? 'behind' : 'ahead') + ': sync it to mine' : n.state === 'restarting' ? ('restart in ' + n.restart_in_s + ' s · ' + live) : syncing || (n.state === 'syncing' && n.message ? n.message + ' · ' : '') + live;
|
||||
$('d-node-sub').title = $('d-node-sub').textContent + (n.last_reading_age_s >= 0 ? ' · last reading ' + Math.round(n.last_reading_age_s) + ' s ago' : '');
|
||||
$('d-hash-sub').title = $('d-hash-sub').textContent; $('d-blocks-sub').title = $('d-blocks-sub').textContent;
|
||||
var cell = $('d-node-cell'); cell.classList.toggle('ok', n.state === 'synced'); cell.classList.toggle('bad', n.state === 'failed' || n.state === 'restarting' || n.state === 'stopped' || (s.clock && s.clock.severity === 'block'));
|
||||
if (n.daa > 0 && p.eta_s >= 0) { $('d-eta').textContent = hms(p.eta_s); $('d-eta-sub').textContent = p.message + (p.epoch_index ? ' (epoch ' + p.epoch_index + ')' : ''); }
|
||||
else { $('d-eta').textContent = '--:--'; $('d-eta-sub').textContent = 'waiting for the node'; }
|
||||
else { $('d-eta').textContent = '--:--'; $('d-eta-sub').textContent = n.state === 'syncing' ? 'after the node is synced' : 'waiting for the node'; }
|
||||
$('d-eta-sub').title = $('d-eta-sub').textContent;
|
||||
var nowU = s.now;
|
||||
$('d-found-10').textContent = m.found.filter(function (t) { return nowU - t <= 600; }).length;
|
||||
$('d-found-60').textContent = m.found.length;
|
||||
$('d-chain-blocks').textContent = compact(n.blocks);
|
||||
// per-card tiles (enabled cards), the rest in one muted line
|
||||
var on = m.cards.filter(function (c) { return c.enabled; }), off = m.cards.filter(function (c) { return !c.enabled; });
|
||||
$('d-cards-eyebrow').textContent = on.length + ' worker' + (on.length === 1 ? '' : 's') + (m.cards.length > on.length ? ' of ' + m.cards.length + ' GPUs' : '');
|
||||
$('d-cards-eyebrow').textContent = on.length + ' worker' + (on.length === 1 ? '' : 's') + (m.cards.length > on.length ? ' of ' + m.cards.length + ' GPU' + (m.cards.length === 1 ? '' : 's') : '');
|
||||
var tb = $('d-cards');
|
||||
if (!on.length) tb.innerHTML = '<div class="empty">' + (m.cards.length ? 'No card switched on. Open settings to pick one.' : 'No GPU worker. The node runs on its own.') + '</div>';
|
||||
else tb.innerHTML = on.map(function (cd) {
|
||||
|
|
@ -398,7 +638,12 @@
|
|||
}
|
||||
// bottom
|
||||
$('btn-pause').textContent = m.paused ? 'Resume' : 'Pause';
|
||||
$('foot-status').textContent = (s.quitting ? 'stopping: miners first, then the node' : (s.display_name ? s.display_name + ' · ' : '') + (m.state === 'mining' ? 'mining' : m.state) + ' · node ' + nodeWord + ' · up ' + uptime(s.uptime_s) + (s.address.display ? ' · rewards to ' + s.address.display.slice(0, 8) + '…' + s.address.display.slice(-4) : ''));
|
||||
// the bottom line: the machine name truncates first, the address is always short, the rest never wraps
|
||||
var foot = s.quitting ? '<span class="pc">stopping: miners first, then the node</span>' :
|
||||
(s.display_name ? '<span class="pc nm" title="' + esc(s.display_name) + '">' + esc(s.display_name) + '</span>' : '') +
|
||||
'<span class="pc">' + esc(m.state === 'mining' ? 'mining' : m.state) + '</span><span class="pc">node ' + esc(nodeWord) + '</span><span class="pc up">up ' + uptime(s.uptime_s) + '</span>' +
|
||||
(s.address.display ? '<span class="pc ad" title="rewards to ' + esc(s.address.display) + '">rewards to ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) + '</span>' : '');
|
||||
if (foot !== lastFoot) { lastFoot = foot; $('foot-status').innerHTML = foot; }
|
||||
$('foot-version').textContent = 'v' + s.version;
|
||||
}
|
||||
function renderPill(s) {
|
||||
|
|
@ -558,9 +803,12 @@
|
|||
if (phase === 'dashboard') renderDashboard(s);
|
||||
if (s.quitting && !$('btn-quit').disabled) { $('btn-quit').disabled = true; }
|
||||
}
|
||||
var failures = 0, first = true;
|
||||
var failures = 0, first = true, pollTick = 0;
|
||||
function poll() {
|
||||
// hidden windows poll every 5 s (the pill and the tray text still need a recent state), visible ones every second
|
||||
if (hiddenNow() && (pollTick++ % 5)) return;
|
||||
api('api/state').then(function (s) {
|
||||
var a = performance.now();
|
||||
failures = 0;
|
||||
if (first) {
|
||||
first = false;
|
||||
|
|
@ -571,11 +819,21 @@
|
|||
if (start === 'cards') renderCards(s);
|
||||
}
|
||||
render(s);
|
||||
perf.render += performance.now() - a; perf.polls++;
|
||||
if (failures === 0 && $('pill').classList.contains('away')) { $('pill').classList.remove('away', 'warn'); }
|
||||
}).catch(function () {
|
||||
failures++;
|
||||
if (failures >= 4) { $('pill').classList.add('warn'); $('pill-text').textContent = state && state.quitting ? 'stopped' : 'engine away'; }
|
||||
if (failures >= 4) { $('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'stopped' : 'engine away'; if (phase === 'dashboard') { $('d-node').textContent = 'no answer'; $('d-node-sub').textContent = 'the engine is not answering; the window reconnects by itself'; $('d-node-cell').classList.add('bad'); } }
|
||||
});
|
||||
}
|
||||
poll(); setInterval(poll, 1000);
|
||||
size();
|
||||
// debug: time N synchronous calls of each hot path (ms per call), independent of the host's frame rate
|
||||
if (debug) window.__igneumBench = function (n) {
|
||||
n = n || 100; var out = {}, a, i;
|
||||
a = performance.now(); for (i = 0; i < n; i++) draw(performance.now()); out.drawMsPerFrame = +((performance.now() - a) / n).toFixed(3);
|
||||
if (state) { a = performance.now(); for (i = 0; i < n; i++) render(state); out.renderMsPerPoll = +((performance.now() - a) / n).toFixed(3); }
|
||||
if (drawerOpen()) { a = performance.now(); for (i = 0; i < n; i++) renderLog(true); out.logMsPerRender = +((performance.now() - a) / n).toFixed(3); }
|
||||
return out;
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -309,8 +309,10 @@
|
|||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- the log drawer -->
|
||||
<div class="drawer" id="drawer">
|
||||
<!-- the log drawer: chips filter by source, search filters by text, the ruler and the jump controls move in time.
|
||||
The list is virtualised (fixed row height, only the visible rows are in the DOM) so 20,000 lines scroll smoothly. -->
|
||||
<div class="drawer" id="drawer" aria-label="Logs">
|
||||
<div class="drawer-grip" id="drawer-grip" title="Drag to resize"><i></i></div>
|
||||
<div class="drawer-head">
|
||||
<div class="chips" id="log-chips">
|
||||
<button class="chip on" data-src="">all</button>
|
||||
|
|
@ -319,17 +321,37 @@
|
|||
<button class="chip" data-src="miner">miner</button>
|
||||
<button class="chip" data-src="watch">watch</button>
|
||||
</div>
|
||||
<label class="check small"><input type="checkbox" id="log-follow" checked><span>follow</span></label>
|
||||
<div class="log-search">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg>
|
||||
<input type="search" id="log-search" placeholder="search" spellcheck="false" autocomplete="off" aria-label="Search the log">
|
||||
<span class="count mono" id="log-count"></span>
|
||||
</div>
|
||||
<div class="log-nav">
|
||||
<input type="text" class="mono" id="log-time" placeholder="HH:MM:SS" maxlength="8" spellcheck="false" autocomplete="off" aria-label="Jump to a time" title="Jump to a time (Enter)">
|
||||
<button class="chip" id="log-last-error" title="Jump to the last error">last error</button>
|
||||
<button class="chip" id="log-last-swap" title="Jump to the last program swap">last swap</button>
|
||||
</div>
|
||||
<div class="log-tools">
|
||||
<span class="mono dim at" id="log-at" title="Time of the top line in view"></span>
|
||||
<button class="chip" id="log-wrap" title="Wrap long lines (up to 5,000 lines)">wrap</button>
|
||||
<button class="chip" id="log-copy" title="Copy the lines in view">copy</button>
|
||||
<label class="check small"><input type="checkbox" id="log-follow" checked><span>follow</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="log" id="log"></pre>
|
||||
<div class="log-ruler" id="log-ruler" title="Click to jump"><span class="t0 mono" id="log-ruler-0"></span><span class="t1 mono" id="log-ruler-1"></span><i class="win" id="log-ruler-win"></i></div>
|
||||
<div class="log-view" id="log-view">
|
||||
<div class="log-pad" id="log-pad"><pre class="log" id="log"></pre></div>
|
||||
<div class="log-empty mono" id="log-empty" hidden>Nothing logged yet.</div>
|
||||
</div>
|
||||
<button class="btn small log-jump" id="log-jump" hidden><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12l7 7 7-7"></path></svg><span id="log-jump-text">Newest</span></button>
|
||||
</div>
|
||||
|
||||
<footer class="bottom" id="bottom" hidden>
|
||||
<div class="left">
|
||||
<button class="btn small" id="btn-pause">Pause</button>
|
||||
<button class="btn small ghost" id="btn-logs">Logs</button>
|
||||
<button class="btn small ghost" id="btn-logs" aria-expanded="false" aria-controls="drawer"><span id="btn-logs-text">Logs</span><svg class="chev" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 15 6-6 6 6"></path></svg></button>
|
||||
</div>
|
||||
<div class="mid mono" id="foot-status"></div>
|
||||
<div class="mid mono" id="foot-status" aria-live="off"></div>
|
||||
<div class="right">
|
||||
<span class="mono dim" id="foot-version"></span>
|
||||
<button class="btn small ghost danger" id="btn-quit">Quit</button>
|
||||
|
|
|
|||
1
tools/ui-mock/recorded/log.json
Normal file
1
tools/ui-mock/recorded/log.json
Normal file
File diff suppressed because one or more lines are too long
1
tools/ui-mock/recorded/state.json
Normal file
1
tools/ui-mock/recorded/state.json
Normal file
File diff suppressed because one or more lines are too long
178
tools/ui-mock/server.mjs
Normal file
178
tools/ui-mock/server.mjs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env node
|
||||
// A stand-in for the Igneum Miner engine, for working on app/igneum-app/ui without touching a running miner.
|
||||
// Serves the UI files at /t/mock/ (the same path shape as the engine) and replays the recorded API responses in
|
||||
// recorded/ with every timestamp shifted to now. POSTs are logged and answered {ok:true}; a few (pause, resume,
|
||||
// settings) are applied to the in-memory state so the dashboard reacts. Nothing here reaches the real engine.
|
||||
//
|
||||
// node tools/ui-mock/server.mjs [port] default 4310
|
||||
// open http://127.0.0.1:4310/t/mock/?scenario=live
|
||||
//
|
||||
// Scenarios (set with ?scenario= on the page URL, or GET /mock/scenario?set=<name>):
|
||||
// live the recorded state as it was (Apple M5 Max mining, node synced)
|
||||
// syncing node syncing, 41,000 headers, blocks climbing at a steady rate (the ETA)
|
||||
// nodedown node failed and restarting, miner waiting
|
||||
// nocards no GPU found (cards screen with ?screen=cards, and the dashboard)
|
||||
// integrated one integrated card only, off by default
|
||||
// clock clock skew that blocks mining
|
||||
// paused miner paused
|
||||
// biglog 12,000 log lines with errors and program swaps (virtualised list)
|
||||
// fresh setup not done (welcome, cards, address screens)
|
||||
// job a remote job running
|
||||
// The log grows by a few lines a second in every scenario, so follow and scroll-to-bottom can be exercised.
|
||||
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const uiDir = path.resolve(here, '../../app/igneum-app/ui');
|
||||
const coin = path.resolve(here, '../../brand/igneum-coin-1024.png');
|
||||
const port = parseInt(process.argv[2] || '4310', 10);
|
||||
const PREFIX = '/t/mock';
|
||||
|
||||
const recState = JSON.parse(fs.readFileSync(path.join(here, 'recorded/state.json'), 'utf8'));
|
||||
const recLog = JSON.parse(fs.readFileSync(path.join(here, 'recorded/log.json'), 'utf8')).lines;
|
||||
const t0 = Date.now() / 1000; // server start
|
||||
const shift = t0 - recState.now; // recorded clock -> live clock
|
||||
|
||||
let scenario = 'live';
|
||||
let paused = false;
|
||||
let seq = 0;
|
||||
let log = [];
|
||||
const posts = [];
|
||||
|
||||
function clone(v) { return JSON.parse(JSON.stringify(v)); }
|
||||
function now() { return Date.now() / 1000; }
|
||||
|
||||
function seedLog() {
|
||||
seq = 0; log = [];
|
||||
for (const l of recLog) log.push({ seq: ++seq, t: l.t + shift, src: l.src, err: l.err, text: l.text });
|
||||
if (scenario === 'biglog') {
|
||||
const base = log[log.length - 1].t - 3600 * 2;
|
||||
const srcs = ['node', 'node', 'node', 'miner1', 'watch', 'app'];
|
||||
for (let i = 0; i < 12000; i++) {
|
||||
const src = srcs[i % srcs.length];
|
||||
const t = base + i * 0.6;
|
||||
let text, err = false;
|
||||
if (i % 1500 === 1499) { src === 'app'; text = '[build] hourly program changed: new program ' + i; }
|
||||
else if (i % 977 === 500) { err = true; text = 'worker: kernel launch failed (code 702) on identity mac-d937c69d-1; restarting in 5 s'; }
|
||||
else if (src === 'node') text = '2026-10-04 18:00:00.000+01:00 [INFO ] PoW accepted ' + (i * 2654435761 >>> 0).toString(16).padStart(8, '0') + 'c954de57e97dc514cd8a92d by igneum-lottery-v2-bound (daa ' + (30000 + i) + ', nonce 0x' + (i * 97).toString(16) + ')';
|
||||
else if (src === 'miner1') text = (t).toFixed(3) + ' ACCEPTED block nonce=0x' + (i * 31).toString(16) + ' (gpu worker, cpu re-check ok, identity mac-d937c69d-1)';
|
||||
else if (src === 'watch') text = t.toFixed(3) + ' node1 blocks=' + (30000 + i) + ' headers=' + (30000 + i) + ' daa=' + (30000 + i) + ' tips=1 peers=4 synced=true';
|
||||
else text = 'status: accepted ' + (5000 + i / 6 | 0) + ' blocks, 17.0 MH/s, mining | node ' + (30000 + i) + ' blocks, 4 peers, synced';
|
||||
log.push({ seq: ++seq, t, src: i % 1500 === 1499 ? 'app' : src, err, text });
|
||||
}
|
||||
log.sort((a, b) => a.t - b.t); log.forEach((l, i) => { l.seq = i + 1; }); seq = log.length;
|
||||
}
|
||||
}
|
||||
// a trickle of new lines so the drawer has something to follow
|
||||
let tick = 0;
|
||||
function growLog() {
|
||||
const t = now(); tick++;
|
||||
const h = (t * 1000 | 0).toString(16);
|
||||
log.push({ seq: ++seq, t, src: 'node', err: false, text: new Date(t * 1000).toISOString().replace('T', ' ').slice(0, 23) + ' [INFO ] PoW accepted ' + h + 'c954de57e97dc514cd8a92d by igneum-lottery-v2-bound (daa ' + (39571 + tick) + ')' });
|
||||
if (tick % 4 === 0) log.push({ seq: ++seq, t, src: 'miner1', err: false, text: t.toFixed(3) + ' ACCEPTED block nonce=0x' + h + ' (gpu worker, cpu re-check ok, identity mac-d937c69d-1)' });
|
||||
if (tick % 10 === 0) log.push({ seq: ++seq, t, src: 'watch', err: false, text: t.toFixed(3) + ' node1 blocks=' + (39571 + tick) + ' headers=' + (39571 + tick) + ' tips=1 peers=4 synced=true' });
|
||||
if (tick % 45 === 0) log.push({ seq: ++seq, t, src: 'app', err: true, text: '[error] mock: a sample error line so "last error" has somewhere to jump' });
|
||||
if (tick % 120 === 60) log.push({ seq: ++seq, t, src: 'app', err: false, text: '[build] hourly program changed: new program (mock swap)' });
|
||||
while (log.length > 40000) log.shift();
|
||||
}
|
||||
setInterval(growLog, 1000);
|
||||
|
||||
function stateNow() {
|
||||
const s = clone(recState);
|
||||
const t = now();
|
||||
s.now = t;
|
||||
s.uptime_s = recState.uptime_s + Math.floor(t - t0);
|
||||
s.mining.found = recState.mining.found.map(x => x + shift).filter(x => t - x < 3600);
|
||||
s.events = recState.events.map(e => ({ ...e, t: e.t + shift }));
|
||||
s.finality.last_lock_at += shift; s.finality.age_s = t - s.finality.last_lock_at;
|
||||
s.update.checked_at += shift; s.clock.checked_at += shift; s.jobs.checked_at += shift;
|
||||
s.program.eta_s = Math.max(0, Math.round(3600 - ((t - t0) % 3600)));
|
||||
s.node.blocks = recState.node.blocks + Math.floor(t - t0); s.node.headers = s.node.blocks; s.node.daa = s.node.blocks;
|
||||
s.node.last_reading_age_s = (t - t0) % 10;
|
||||
// a live-looking hash rate
|
||||
const wob = Math.sin((t - t0) / 7) * 1.4;
|
||||
s.mining.hash_total = +(16.9 + wob).toFixed(2); s.mining.cards[0].hash_now = s.mining.hash_total;
|
||||
// a fresh block every ~9 s on average
|
||||
const extra = []; for (let k = t0; k < t; k += 9.3) extra.push(k + 0.4);
|
||||
s.mining.found = s.mining.found.concat(extra).filter(x => t - x < 3600).sort((a, b) => a - b);
|
||||
s.mining.accepted_session += extra.length; s.mining.accepted_total += extra.length; s.mining.cards[0].accepted += extra.length;
|
||||
if (extra.length) s.events = [{ kind: 'block', t: extra[extra.length - 1], text: 'block accepted by the node (Apple M5 Max)' }].concat(s.events).slice(0, 40);
|
||||
s.display_name = 'studio-mac-pro-with-a-long-name';
|
||||
s.host = s.display_name;
|
||||
|
||||
if (paused || scenario === 'paused') { s.mining.paused = true; s.mining.state = 'paused'; s.mining.cards[0].state = 'off'; s.mining.hash_total = 0; s.mining.cards[0].hash_now = 0; }
|
||||
if (scenario === 'syncing') {
|
||||
const rate = 38; // blocks per second
|
||||
s.node.state = 'syncing'; s.node.synced = false; s.node.headers = 412000; s.node.blocks = Math.min(412000, 30000 + Math.floor((t - t0) * rate)); s.node.daa = s.node.blocks;
|
||||
s.node.message = 'downloading blocks'; s.node.peers = 2; s.node.tips = 0;
|
||||
s.mining.state = 'waiting'; s.mining.hash_total = 0; s.mining.cards[0].state = 'waiting'; s.mining.cards[0].hash_now = 0; s.mining.found = []; s.program.eta_s = -1; s.program.message = '';
|
||||
s.finality = { age_s: 0, last_lock: 0, last_lock_at: 0, message: 'Finality starts after the node is synced.', votes: 0 };
|
||||
}
|
||||
if (scenario === 'nodedown') {
|
||||
s.node.state = 'restarting'; s.node.synced = false; s.node.restart_in_s = 7 - Math.floor(t - t0) % 8; s.node.restarts = 3; s.node.peers = 0; s.node.message = 'the node exited (code 101)';
|
||||
s.mining.state = 'waiting'; s.mining.hash_total = 0; s.mining.cards[0].state = 'waiting'; s.mining.cards[0].hash_now = 0; s.mining.found = []; s.program.eta_s = -1;
|
||||
s.events = [{ kind: 'error', t: t - 12, text: 'node exited with code 101; restarting in 8 s' }].concat(s.events).slice(0, 40);
|
||||
}
|
||||
if (scenario === 'nocards') { s.mining.cards = []; s.mining.state = 'idle'; s.mining.hash_total = 0; s.mining.found = []; s.detect_message = 'No Metal, CUDA or OpenCL device answered.'; s.detecting = false; }
|
||||
if (scenario === 'integrated') {
|
||||
s.mining.cards = [{ ...recState.mining.cards[0], key: 'intel::Intel Iris Xe', name: 'Intel Iris Xe Graphics', kind: 'integrated', vendor: 'other', worker: 'OpenCL', detail: '96 EUs, shared memory', vram_mb: 0, enabled: false, state: 'off', hash_now: 0, hash_avg: 0, accepted: 0, ids: [], reason: 'integrated graphics: off by default, around 0.3 MH/s and it slows the desktop', identities: 1, prepared: false }];
|
||||
s.mining.state = 'idle'; s.mining.hash_total = 0; s.mining.found = [];
|
||||
}
|
||||
if (scenario === 'clock') { s.clock = { ...s.clock, severity: 'block', skew_s: -412, message: 'This machine\'s clock is 6 min 52 s behind the network. Blocks it finds are rejected until the clock is right.', hint: 'System Settings > General > Date & Time: turn on "Set time and date automatically"' }; s.mining.state = 'waiting'; s.mining.hash_total = 0; }
|
||||
if (scenario === 'fresh') { s.setup_done = false; s.phase = 'welcome'; s.address = { display: '', key_saved: false, source: '', value: '', wallet_file: '' }; }
|
||||
if (scenario === 'job') { s.jobs = { ...s.jobs, active: true, id: 'job-2026-10-04-17', kind: 'shard-benchmark', title: 'shard benchmark', stage: 'proving shard 3 of 8', started_at: t - 400, results: ['RESULT shard=1 prove_s=41.2', 'RESULT shard=2 prove_s=39.8'], message: '' }; }
|
||||
if (scenario === 'nvidia') {
|
||||
s.mining.cards = [{ ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: 441, temp_gpu: 71, temp_mem: 92, telemetry_at: t, hash_now: 412.5, hash_avg: 405.2 },
|
||||
{ ...recState.mining.cards[0], key: 'nvidia:1:NVIDIA GeForce RTX 4070', name: 'NVIDIA GeForce RTX 4070', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '5888 cores', vram_mb: 12288, device: '1', identities: 2, ids: ['9a8b7c6d'], power_default_w: 200, power_pct: 80, power_applied: false, power_note: 'nvidia-smi: wanted 160 W, card reports 200 W', power_limit_w: 200, power_w: 188, temp_gpu: 64, temp_mem: 0, telemetry_at: t, hash_now: 98.1, hash_avg: 97.4, state: 'restarting', restart_in_s: 4 },
|
||||
{ ...recState.mining.cards[0], key: 'intel::Intel UHD 770', name: 'Intel UHD Graphics 770', kind: 'integrated', vendor: 'other', worker: 'OpenCL', detail: '32 EUs', vram_mb: 0, enabled: false, state: 'off', hash_now: 0, hash_avg: 0, accepted: 0, ids: [], reason: 'integrated graphics: off by default', identities: 1 }];
|
||||
s.mining.hash_total = 510.6; s.display_name = 'rig-01';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const types = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2' };
|
||||
|
||||
function send(res, code, type, body, cache) {
|
||||
res.writeHead(code, { 'Content-Type': type, 'Cache-Control': cache ? 'public, max-age=86400' : 'no-store', 'Content-Length': Buffer.byteLength(body) });
|
||||
res.end(body);
|
||||
}
|
||||
function json(res, v) { send(res, 200, 'application/json; charset=utf-8', JSON.stringify(v)); }
|
||||
|
||||
seedLog();
|
||||
http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://127.0.0.1');
|
||||
if (u.pathname === '/mock/scenario') {
|
||||
if (u.searchParams.get('set')) { scenario = u.searchParams.get('set'); paused = false; seedLog(); }
|
||||
return json(res, { scenario, lines: log.length, posts: posts.slice(-20) });
|
||||
}
|
||||
if (u.pathname === '/mock/posts') return json(res, posts);
|
||||
if (!u.pathname.startsWith(PREFIX)) return send(res, 404, 'text/plain', 'not found');
|
||||
let rest = u.pathname.slice(PREFIX.length) || '/';
|
||||
if (rest === '/' || rest === '/index.html') {
|
||||
if (u.searchParams.get('scenario') && u.searchParams.get('scenario') !== scenario) { scenario = u.searchParams.get('scenario'); paused = false; seedLog(); }
|
||||
return send(res, 200, types['.html'], fs.readFileSync(path.join(uiDir, 'index.html')));
|
||||
}
|
||||
if (rest === '/api/state' && req.method === 'GET') return json(res, stateNow());
|
||||
if (rest === '/api/log' && req.method === 'GET') {
|
||||
const after = parseInt(u.searchParams.get('after') || '0', 10), limit = Math.min(20000, parseInt(u.searchParams.get('limit') || '400', 10));
|
||||
let out = log.filter(l => l.seq > after); if (out.length > limit) out = out.slice(out.length - limit);
|
||||
return json(res, { lines: out });
|
||||
}
|
||||
if (req.method === 'POST') {
|
||||
let body = ''; req.on('data', c => { body += c; }); req.on('end', () => {
|
||||
posts.push({ t: now(), path: rest, body: body.slice(0, 300) }); console.log('POST', rest, body.slice(0, 200));
|
||||
if (rest === '/api/pause') paused = true; if (rest === '/api/resume') paused = false;
|
||||
if (rest === '/api/key/reveal') return json(res, { ok: true, private_key: '0x' + 'ab'.repeat(32) });
|
||||
if (rest === '/api/setup') return json(res, { ok: true, address: '0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8', display: '0xdd442fCbb964A3aFDc90D49B408e8DD296FA86E8', private_key: '0x' + 'cd'.repeat(32), wallet_file: '/Users/you/Library/Application Support/Igneum/app/wallet.json' });
|
||||
json(res, { ok: true });
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (rest === '/coin.png') return send(res, 200, types['.png'], fs.readFileSync(coin), true);
|
||||
const file = path.normalize(path.join(uiDir, rest));
|
||||
if (!file.startsWith(uiDir) || !fs.existsSync(file) || fs.statSync(file).isDirectory()) return send(res, 404, 'text/plain', 'not found');
|
||||
send(res, 200, types[path.extname(file)] || 'application/octet-stream', fs.readFileSync(file), rest.startsWith('/fonts/'));
|
||||
}).listen(port, '127.0.0.1', () => console.log('ui-mock on http://127.0.0.1:' + port + PREFIX + '/ scenario=' + scenario));
|
||||
Loading…
Reference in a new issue