diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index 4428c6c9e..cd608c44b 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -195,11 +195,6 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card /* GPU rows (first run) */ .gpu-row{display:flex;align-items:center;gap:var(--s-4);background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0;flex-wrap:wrap} .gpu-row.off>.badge,.gpu-row.off>.switch,.gpu-row.off .info>:not(.driver){opacity:.72} -.badge{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 44px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} -.badge.apple{color:var(--bone)} -.badge.nvidia{color:var(--nvidia)} -.badge.amd{color:var(--ember)} -.badge.intel{color:var(--intel)} .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} @@ -479,18 +474,21 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} .row{display:flex;gap:10px;align-items:center;min-width:0} .row.wrap{flex-wrap:wrap} .row .addr-input{margin-top:0;min-width:0} -.switch{display:flex;align-items:flex-start;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:9px 0;line-height:1.4} +/* the custom switch (Prove page fix, 7 October 2026): the label is the positioned ancestor, the native input is hidden + the accessible way (1 px, clipped, still focusable, the label association kept), the knob lives inside the track at + both states; the size class is the switch's own (big), never the DAG legend's swatch class */ +.switch{display:flex;align-items:flex-start;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:9px 0;line-height:1.4;position:relative} .switch+.switch{border-top:1px solid var(--line)} -.switch input{position:absolute;opacity:0;width:0;height:0} +.switch input{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap} .switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease;margin-top:1px} .switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#F4F1EC;transition:transform .15s ease} .switch input:checked+.track{background:var(--ember)} .switch input:checked+.track::after{transform:translateX(18px)} .switch input:focus-visible+.track{outline:2px solid var(--ember);outline-offset:3px} .switch input:disabled+.track{opacity:.4} -.switch.lg .track{width:52px;height:30px;flex-basis:52px} -.switch.lg .track::after{width:24px;height:24px} -.switch.lg input:checked+.track::after{transform:translateX(22px)} +.switch.big .track{width:52px;height:30px;flex-basis:52px} +.switch.big .track::after{width:24px;height:24px} +.switch.big input:checked+.track::after{transform:translateX(22px)} .sw-text{min-width:0} .sw-text b{font-weight:600} .sw-text .dim{font-size:var(--t-base)} @@ -669,13 +667,14 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .dag-chip{position:absolute;right:10px;top:10px;font-size:var(--t-xs);color:var(--ash);background:var(--graphite);border:1px solid var(--line-2);border-radius:999px;padding:3px 10px;pointer-events:none;z-index:2} .legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ash);align-items:center} .legend span{display:inline-flex;align-items:center;gap:7px} -.lg{width:10px;height:10px;border-radius:3px;display:inline-block;border:1.5px solid var(--ash);background:var(--row)} -.lg.you{border-color:var(--molten);box-shadow:0 0 0 2px var(--molten-40)} -.lg.pending{border-color:var(--ash)} -.lg.included{border-color:var(--ember)} -.lg.excluded{border-color:var(--ash);opacity:.45} -.lg.proven{background:var(--ember);border-color:var(--ember)} -.lg.locked{border-color:var(--ember);border-radius:50%;box-shadow:0 0 0 2px var(--ember-40)} +/* the DAG legend swatches, scoped to the legend (a bare .lg reached label.switch.lg until 7 October 2026) */ +.legend .lg{width:10px;height:10px;border-radius:3px;display:inline-block;border:1.5px solid var(--ash);background:var(--row)} +.legend .lg.you{border-color:var(--molten);box-shadow:0 0 0 2px var(--molten-40)} +.legend .lg.pending{border-color:var(--ash)} +.legend .lg.included{border-color:var(--ember)} +.legend .lg.excluded{border-color:var(--ash);opacity:.45} +.legend .lg.proven{background:var(--ember);border-color:var(--ember)} +.legend .lg.locked{border-color:var(--ember);border-radius:50%;box-shadow:0 0 0 2px var(--ember-40)} /* Cards: the Tuning strip above the list */ .tune-strip{padding:var(--s-4) var(--card-pad)} .ts-row{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap} @@ -804,9 +803,10 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .proof-scene{height:180px;border:1px solid var(--line);border-radius:6px;background:var(--row);margin:10px 0;overflow:hidden} .proof-scene canvas{display:block;width:100%;height:100%} .proof-head{display:flex;justify-content:space-between;align-items:baseline;font-weight:500;color:var(--bone);margin-bottom:8px} -.track{display:flex;gap:3px;height:3px;margin-bottom:10px} -.track i{flex:1 1 0;background:var(--line);border-radius:2px} -.track i.proving{background:var(--ember)}.track i.verified{background:var(--bone)}.track i.paid{background:var(--molten)} +/* the inspector's shard track, scoped to its element (a bare .track reached every switch's track until 7 October 2026) */ +#i-track{display:flex;gap:3px;height:3px;margin-bottom:10px} +#i-track i{flex:1 1 0;background:var(--line);border-radius:2px} +#i-track i.proving{background:var(--ember)}#i-track i.verified{background:var(--bone)}#i-track i.paid{background:var(--molten)} .shards{list-style:none;margin:0 0 12px;padding:0;font-size:var(--t-xs);line-height:1.7;max-height:120px;overflow:auto} .shards li{display:flex;justify-content:space-between;gap:8px;color:var(--ink-2)} .shards li::before{content:"\25CF";font-size:7px;margin-right:6px;color:var(--line-2)} @@ -818,3 +818,34 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .inspector .note{font-size:var(--t-sm);margin-top:12px;padding-top:10px;border-top:1px solid var(--line)} .inspector .top-gap{margin-top:8px} @media (max-width:900px){.cf-graph{grid-template-columns:1fr}.inspector{border-left:0;padding-left:0;border-top:1px solid var(--line);padding-top:var(--s-3)}} + +/* ---------- vendor marks (gpu-logos, 7 October 2026): a self-contained block, the last thing in the file ---------- + One simplified monochrome glyph per GPU vendor (View.vendorMark in app.js) in a soft rounded well: the vendor colour + at low alpha behind it, a hairline ring in the same colour, the glyph in the full colour. The tokens are the + module's own (--mark-*), one set per theme, so the light hex of every vendor reads at 3:1 or better on its well + (view.test.mjs computes the ratio and checks the hex here). The ember accent is for state and never tints a brand. + Nothing animates: hover and focus-within only deepen the ring. One mark per row, drawn by View.markHtml alone. */ +:root{--mark-nvidia:#8BE37A;--mark-nvidia-well:rgba(139,227,122,.14);--mark-nvidia-ring:rgba(139,227,122,.45);--mark-amd:#FF5A5A;--mark-amd-well:rgba(255,90,90,.14);--mark-amd-ring:rgba(255,90,90,.45);--mark-intel:#7CC4FF;--mark-intel-well:rgba(124,196,255,.14);--mark-intel-ring:rgba(124,196,255,.45);--mark-apple:#E6E3DD;--mark-apple-well:rgba(230,227,221,.14);--mark-apple-ring:rgba(230,227,221,.45);--mark-gpu:#9A9A9E;--mark-gpu-well:rgba(154,154,158,.14);--mark-gpu-ring:rgba(154,154,158,.45)} +@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){--mark-nvidia:#2F8A22;--mark-nvidia-well:rgba(47,138,34,.1);--mark-nvidia-ring:rgba(47,138,34,.45);--mark-amd:#C41E2A;--mark-amd-well:rgba(196,30,42,.1);--mark-amd-ring:rgba(196,30,42,.45);--mark-intel:#1C6FD6;--mark-intel-well:rgba(28,111,214,.1);--mark-intel-ring:rgba(28,111,214,.45);--mark-apple:#4A4A50;--mark-apple-well:rgba(74,74,80,.1);--mark-apple-ring:rgba(74,74,80,.45);--mark-gpu:#6B6B70;--mark-gpu-well:rgba(107,107,112,.1);--mark-gpu-ring:rgba(107,107,112,.45)}} +:root[data-theme="light"]{--mark-nvidia:#2F8A22;--mark-nvidia-well:rgba(47,138,34,.1);--mark-nvidia-ring:rgba(47,138,34,.45);--mark-amd:#C41E2A;--mark-amd-well:rgba(196,30,42,.1);--mark-amd-ring:rgba(196,30,42,.45);--mark-intel:#1C6FD6;--mark-intel-well:rgba(28,111,214,.1);--mark-intel-ring:rgba(28,111,214,.45);--mark-apple:#4A4A50;--mark-apple-well:rgba(74,74,80,.1);--mark-apple-ring:rgba(74,74,80,.45);--mark-gpu:#6B6B70;--mark-gpu-well:rgba(107,107,112,.1);--mark-gpu-ring:rgba(107,107,112,.45)} +.badge{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;flex:0 0 44px;border:1px solid var(--mark-gpu-ring);color:var(--mark-gpu);background:var(--mark-gpu-well);box-shadow:0 0 0 0 transparent;transition:box-shadow .15s ease,border-color .15s ease} +.badge svg{width:22px;height:22px;display:block} +.badge.nvidia{color:var(--mark-nvidia);background:var(--mark-nvidia-well);border-color:var(--mark-nvidia-ring)} +.badge.amd{color:var(--mark-amd);background:var(--mark-amd-well);border-color:var(--mark-amd-ring)} +.badge.intel{color:var(--mark-intel);background:var(--mark-intel-well);border-color:var(--mark-intel-ring)} +.badge.apple{color:var(--mark-apple);background:var(--mark-apple-well);border-color:var(--mark-apple-ring)} +.badge.gpu{color:var(--mark-gpu);background:var(--mark-gpu-well);border-color:var(--mark-gpu-ring)} +.gpu-row:hover .badge,.gpu-line:hover .badge,.gpu-row:focus-within .badge,.gpu-line:focus-within .badge{border-color:currentColor;box-shadow:0 0 0 3px var(--mark-gpu-well)} +.gpu-row:hover .badge.nvidia,.gpu-line:hover .badge.nvidia,.gpu-row:focus-within .badge.nvidia,.gpu-line:focus-within .badge.nvidia{box-shadow:0 0 0 3px var(--mark-nvidia-well)} +.gpu-row:hover .badge.amd,.gpu-line:hover .badge.amd,.gpu-row:focus-within .badge.amd,.gpu-line:focus-within .badge.amd{box-shadow:0 0 0 3px var(--mark-amd-well)} +.gpu-row:hover .badge.intel,.gpu-line:hover .badge.intel,.gpu-row:focus-within .badge.intel,.gpu-line:focus-within .badge.intel{box-shadow:0 0 0 3px var(--mark-intel-well)} +.gpu-row:hover .badge.apple,.gpu-line:hover .badge.apple,.gpu-row:focus-within .badge.apple,.gpu-line:focus-within .badge.apple{box-shadow:0 0 0 3px var(--mark-apple-well)} +.badge.mini{width:26px;height:26px;border-radius:8px;flex:0 0 26px;display:inline-flex;vertical-align:middle;margin-right:8px} +.badge.mini svg{width:15px;height:15px} +@media (prefers-reduced-motion:reduce){.badge{transition:none}} +/* the series line under the name: mono, small, quiet */ +.gen{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} +.gpu-row .gen{margin-top:-2px} +.gpu-line .who .gen{margin:-1px 0 1px} +#bc-card{display:flex;align-items:center;min-width:0} +#bc-card>span{min-width:0;overflow:hidden;text-overflow:ellipsis} diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index f314f738b..661954fc1 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -305,6 +305,84 @@ var View = (function () { // the tooltip on a card's name: what the tool calls it (gfx1201), its device index, the OpenCL platform, the PCI address function cardTitle(cd) { var t = []; if (cd.code && cd.code !== cd.name) t.push(cd.code); if (cd.device !== undefined && cd.device !== '') t.push(cd.worker + ' device ' + cd.device); if (cd.platform) t.push(cd.platform); if (cd.bus) t.push('bus ' + cd.bus); return t.join(' · '); } + // ---------- vendor marks (gpu-logos, 7 October 2026) ---------- + // One simplified monochrome glyph per GPU vendor, drawn by hand from the public mark's geometry (never a copied logo + // file, never a raster): a nominative use that names the hardware. Each glyph is under 2 KB and takes currentColor, + // so the per-vendor token tints it; the ember accent is reserved for state and never tints a brand. The light hex of + // every vendor reads at 3:1 or better on its well (the vendor colour at WELL_ALPHA_LIGHT over the graphite card); + // view.test.mjs computes the ratio and checks app.css carries the same hex values. + var WELL_ALPHA_LIGHT = 0.10, WELL_ALPHA_DARK = 0.14; + var VENDORS = { + nvidia: { label: 'NVIDIA', dark: '#8BE37A', light: '#2F8A22' }, + amd: { label: 'AMD Radeon', dark: '#FF5A5A', light: '#C41E2A' }, + intel: { label: 'Intel', dark: '#7CC4FF', light: '#1C6FD6' }, + apple: { label: 'Apple', dark: '#E6E3DD', light: '#4A4A50' }, + gpu: { label: 'GPU', dark: '#9A9A9E', light: '#6B6B70' } + }; + var SVG_OPEN = '', + // the arrow: the corner bracket pointing up and right, the bracket below it + amd: SVG_OPEN + ' fill="currentColor">', + // the swoosh: an open tilted ring around the dot of the i + intel: SVG_OPEN + ' fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">', + // the apple: the bitten fruit with its leaf + apple: SVG_OPEN + ' fill="currentColor">', + // the neutral fallback: a chip with its pins + gpu: SVG_OPEN + ' fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round">' + }; + function escH(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } + // the vendor a row is drawn as: the engine's word when it is one of the four, else the name decides (an OpenCL + // integrated row arrives as vendor "other" with "Intel UHD Graphics 770"), else the neutral chip + function vendorOf(cd) { + var v = (cd && cd.vendor || '').toLowerCase(); + if (VENDORS[v] && v !== 'gpu') return v; + var n = (cd && cd.name || '') + ' ' + (cd && cd.code || ''); + if (/nvidia|geforce|quadro|\bRTX\b|\bGTX\b/i.test(n)) return 'nvidia'; + if (/\bamd\b|radeon|\bgfx\d/i.test(n)) return 'amd'; + if (/intel|\barc\b|iris|\buhd\b/i.test(n)) return 'intel'; + if (/apple|\bM\d (max|pro|ultra)\b/i.test(n)) return 'apple'; + return 'gpu'; + } + // the series line under the name, in mono: "RTX 50 series · Blackwell", "RX 9000 series · RDNA 4", "Arc B series · + // Battlemage", "M5 series · integrated"; an integrated row always ends in "integrated" (the same glyph, never a second mark) + function seriesLine(cd) { + var v = vendorOf(cd), n = cd.name || '', code = cd.code || '', m, out = ''; + if (v === 'nvidia') { + if ((m = /\b(RTX|GTX)\s*(\d)(\d)(\d{2})\b/i.exec(n))) { var tens = m[2] + m[3], fam = m[1].toUpperCase(), gen = { RTX50: 'Blackwell', RTX40: 'Ada Lovelace', RTX30: 'Ampere', RTX20: 'Turing', GTX16: 'Turing', GTX10: 'Pascal', GTX90: 'Maxwell' }[fam + tens]; out = fam + ' ' + tens + ' series' + (gen ? ' · ' + gen : ''); } + else if (/\b(H100|H200|A100|B200|B100|L40|L4|A30|A10)\b/i.test(n)) out = 'data centre'; + else if (/\bRTX\b|quadro/i.test(n)) out = 'RTX workstation'; + else out = 'GeForce'; + } else if (v === 'amd') { + if ((m = /\bRX\s*(\d)(\d{3})\b/i.exec(n))) { var g = { 9: 'RDNA 4', 7: 'RDNA 3', 6: 'RDNA 2', 5: 'RDNA' }[m[1]]; out = 'RX ' + m[1] + '000 series' + (g ? ' · ' + g : ''); } + else if (/\bgfx12/i.test(code + n)) out = 'RDNA 4'; else if (/\bgfx11/i.test(code + n)) out = 'RDNA 3'; else if (/\bgfx103/i.test(code + n)) out = 'RDNA 2'; else if (/\bgfx10/i.test(code + n)) out = 'RDNA'; + else if (/\bMI\d{2,3}/i.test(n)) out = 'Instinct · data centre'; + else out = 'Radeon'; + } else if (v === 'intel') { + if ((m = /\bArc\b.*?\b([ABC])(\d{3})\b/i.exec(n)) || (m = /\b([ABC])(\d{3})\b.*\bArc\b/i.exec(n))) { var L = m[1].toUpperCase(); out = 'Arc ' + L + ' series · ' + ({ A: 'Alchemist', B: 'Battlemage', C: 'Celestial' })[L]; } + else if (/iris/i.test(n)) out = 'Iris Xe'; else if (/\buhd\b/i.test(n)) out = 'UHD Graphics'; else out = 'Intel graphics'; + } else if (v === 'apple') { + out = (m = /\bM(\d)\b/.exec(n)) ? 'M' + m[1] + ' series' : 'Apple silicon'; + } else out = cd.worker ? String(cd.worker) : ''; + if (cd.kind === 'integrated' || cd.kind === 'apple') out = (out ? out + ' · ' : '') + 'integrated'; + return out; + } + // the mark for a row: the vendor word, the glyph, the label for the title, the series line + function vendorMark(cd) { var v = vendorOf(cd); return { vendor: v, svg: MARKS[v], label: VENDORS[v].label, series: seriesLine(cd) }; } + function markHtml(cd, size) { var mk = vendorMark(cd); return '
' + mk.svg + '
'; } + // the name block beside the mark: the name, the kind pill, the series line; shared by the first-run rows and the Cards page + function nameHtml(cd, extra) { var mk = vendorMark(cd); return '
' + escH(cd.name) + (extra || '') + ' ' + escH(kindWord(cd.kind)) + '
' + (mk.series ? '
' + escH(mk.series) + '
' : ''); } + // the two together: one mark, one name block + function rowHead(cd, extra) { return markHtml(cd) + nameHtml(cd, extra); } + // sRGB relative luminance and the WCAG contrast ratio, for the test and for anyone picking a new vendor hex + function hexRgb(h) { h = h.replace('#', ''); return [0, 2, 4].map(function (i) { return parseInt(h.substr(i, 2), 16); }); } + function luminance(rgb) { var c = rgb.map(function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; } + function contrast(a, b) { var la = luminance(a), lb = luminance(b), hi = Math.max(la, lb), lo = Math.min(la, lb); return (hi + 0.05) / (lo + 0.05); } + // the well's colour: the vendor hex at alpha over the card ground (graphite) + function wellRgb(hex, alpha, ground) { var v = hexRgb(hex), g = hexRgb(ground); return v.map(function (c, i) { return Math.round(c * alpha + g[i] * (1 - alpha)); }); } + function vendorContrast(v, theme) { var hex = VENDORS[v][theme], ground = theme === 'light' ? '#FFFFFF' : '#16161A', alpha = theme === 'light' ? WELL_ALPHA_LIGHT : WELL_ALPHA_DARK; return contrast(hexRgb(hex), wellRgb(hex, alpha, ground)); } + // one GPU row on the Mine page: cd = state.mining.cards[i] function cardRow(cd) { var removed = cd.removed_at > 0, unusable = !!cd.problem; @@ -1000,7 +1078,7 @@ var View = (function () { var addr = (s.address || {}).display || ''; var ignLine = c.minerIgn ? fmtIgn(c.minerIgn) + (addr ? ' to ' + shortHex(addr, 6, 4) : '') + ' (72 producer points, 8 for signing' + (c.ramp && c.ramp < 1 ? ', ramp ' + Math.round(c.ramp * 100) + '%' : '') + ')' : (addr ? 'Paid to ' + shortHex(addr, 6, 4) : ''); var d = daysIn(s.ladder, c.windowS, now), rankLine = (c.best ? 'rank ' + withCommas(c.best.rank) + ' of ' + withCommas(c.listed) + ' keys' : 'rank: not in the table yet') + ' · ' + (d.unit === 'days' ? 'day ' : 'hour ') + Math.max(1, d.have) + ' of ' + d.of; - return { kind: m.kind, count: m.count, eyebrow: eyebrow, title: title, when: m.at, cardLine: cardLine, ignLine: ignLine, rankLine: rankLine, hash: m.hash || '', hashShort: m.hash ? shortHex(m.hash, 8, 6) : 'hash: waiting for the node’s line', url: m.hash ? 'https://igneum.network/block/' + m.hash : '', key: 'card:' + m.count + ':' + Math.floor(m.at || 0), fields: { card: FIELDS.found, ign: FIELDS.reward, rank: FIELDS.rank + ' · ' + FIELDS.days } }; + return { kind: m.kind, count: m.count, eyebrow: eyebrow, title: title, when: m.at, cardLine: cardLine, card: { name: name, vendor: cd.vendor, kind: cd.kind, code: cd.code }, ignLine: ignLine, rankLine: rankLine, hash: m.hash || '', hashShort: m.hash ? shortHex(m.hash, 8, 6) : 'hash: waiting for the node’s line', url: m.hash ? 'https://igneum.network/block/' + m.hash : '', key: 'card:' + m.count + ':' + Math.floor(m.at || 0), fields: { card: FIELDS.found, ign: FIELDS.reward, rank: FIELDS.rank + ' · ' + FIELDS.days } }; } // Earnings (reinvent 3.4): IGN first, the window, a typed price, the share and rank, IGN per kWh. price = £ per IGN // typed by the user (null = none), pence = electricity, now = unix s @@ -1079,7 +1157,7 @@ var View = (function () { } function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? Mining restarts.'; } function trustAsk(on) { return on ? 'Include proofs the node never checked? Devnet only. The node restarts.' : 'Check proofs again? The node restarts.'; } - return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, driverWords: driverWords, vendorWord: vendorWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords }; + return { VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, driverWords: driverWords, vendorWord: vendorWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords }; })(); /* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ---------- One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned @@ -1920,7 +1998,7 @@ if (typeof document !== 'undefined') (function () { if (show) { card.dataset.key = bc.key; card.dataset.url = bc.url; setText('bc-eyebrow', bc.eyebrow); setText('bc-when', bc.when > 0 ? clock(bc.when) : ''); setText('bc-title', bc.title); - setText('bc-card', bc.cardLine); $('bc-card').title = bc.fields.card; + $('bc-card').innerHTML = View.markHtml(bc.card, 'mini') + '' + esc(bc.cardLine) + ''; $('bc-card').title = bc.fields.card; setText('bc-ign', bc.ignLine); $('bc-ign').title = bc.fields.ign; setText('bc-rank', bc.rankLine); $('bc-rank').title = bc.fields.rank; setText('bc-hash', 'hash ' + bc.hashShort); $('bc-hash').title = bc.hash; @@ -1948,10 +2026,7 @@ if (typeof document !== 'undefined') (function () { setText('tl-note', 'Times from install (' + (tl.base > 0 ? whenWords(tl.base) : 'not recorded') + '). The site’s /miner page shows the same steps measured on fresh installs.'); } // ---------- render ---------- - function badgeHtml(cd) { - if (cd.vendor === 'apple') return ''; - return cd.vendor === 'nvidia' ? 'NV' : cd.vendor === 'amd' ? 'AMD' : cd.vendor === 'intel' ? 'INTEL' : 'GPU'; - } + // the vendor mark and the name block come from View.rowHead (one mark per row, never two) function vramText(cd) { if (!cd.vram_mb) return ''; var gb = cd.vram_mb / 1024; return (gb >= 10 ? Math.round(gb) : gb.toFixed(gb < 4 ? 1 : 0)) + ' GB' + (cd.kind === 'apple' ? ' unified' : ''); } // driver-check: the first-run row's driver line (the same words as the dashboard strip, the same buttons) function setupDriverHtml(cd) { @@ -1970,8 +2045,8 @@ if (typeof document !== 'undefined') (function () { d.className = 'gpu-row' + (cd.enabled && !removed && !unusable ? '' : ' off') + (removed ? ' removed' : '') + (unusable ? ' unusable' : ''); d.dataset.key = cd.key; var meta = (vramText(cd) ? '' + esc(vramText(cd)) + '' : '') + (cd.detail && cd.kind !== 'apple' ? '' + esc(cd.detail) + '' : '') + (cd.detail && cd.kind === 'apple' ? '' + esc(cd.detail.split(',')[0]) + '' : ''); - d.innerHTML = '
' + badgeHtml(cd) + '
' + - '
' + esc(cd.name) + (unusable ? ': ' + esc(cd.message || 'not usable (' + cd.problem + ')') : removed ? ': removed' : '') + ' ' + kindWord(cd.kind) + '
' + meta + '
' + + d.innerHTML = View.markHtml(cd) + + '
' + View.nameHtml(cd, unusable ? ': ' + esc(cd.message || 'not usable (' + cd.problem + ')') : removed ? ': removed' : '') + '
' + meta + '
' + (cd.reason ? '
' + esc(cd.reason) + '
' : '') + (cd.message && !unusable && !removed ? '
' + esc(cd.message) + '
' : '') + setupDriverHtml(cd) + '
' + ''; container.appendChild(d); @@ -2058,8 +2133,8 @@ if (typeof document !== 'undefined') (function () { (dw.sub ? '' + esc(dw.sub) + '' : '') + (dw.dry ? 'dry run: the installer is not run' : '') + '
'; var tune = em.kind === 'none' ? '' : '
' + flameHtml(em.fill, em.kind) + '' + esc(em.label) + '' + (em.sub ? '' + esc(em.sub) + '' : '') + (em.savingText ? '' + esc(em.savingText) + '' : '') + (em.heat ? '' + esc(em.heat) + '' : '') + (em.kind === 'running' ? '' : '') + '
'; return '
' + - '
' + badgeHtml({ vendor: r.vendor }) + '
' + - '
' + esc(r.name) + ' ' + esc(r.kindWord) + '
' + + '
' + View.markHtml(cd) + + '
' + View.nameHtml(cd) + '' + esc(word) + (r.meta ? ' · ' + esc(r.meta) : '') + '' + subs.join('') + '
' + '
' + nums + '
' + '
' + tuneBtn + sw + chev + '
' + drv + tune + diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 4e74d13c2..3e05e8f9f 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -429,7 +429,7 @@

Prove on this machine

Every block is turned into a short proof, in pieces called shards. Your cards prove shards and earn IGN for each one.

- +

    diff --git a/app/igneum-app/ui/view.test.mjs b/app/igneum-app/ui/view.test.mjs index 3feddfe3b..d36baf0b0 100644 --- a/app/igneum-app/ui/view.test.mjs +++ b/app/igneum-app/ui/view.test.mjs @@ -725,3 +725,150 @@ test('the block card on the four thresholds and on a new card; the first-hour ti assert.equal(V.plainEvent('block 1,000 of this machine: found by RTX 4070 (block card)').text, 'Block 1,000 of this machine, found by RTX 4070'); assert.equal(V.plainEvent('first block on Apple M5 Max (block card)').text, 'First block on Apple M5 Max'); }); + +// ---------- vendor marks (gpu-logos, 7 October 2026) ---------- +const css = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'app.css'), 'utf8'); +const marks = (html) => (html.match(/data-mark="/g) || []).length; + +test('every vendor renders its own mark: one inline SVG glyph per row, tinted by its vendor class, under 2 KB each', () => { + const rows = { + nvidia: card(), + amd: card({ key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', vendor: 'amd', vram_mb: 16384 }), + intel: card({ key: 'intel:Intel(R) Arc(TM) B580 Graphics', name: 'Intel(R) Arc(TM) B580 Graphics', vendor: 'intel', worker: 'OpenCL', vram_mb: 12208 }), + apple: card({ key: 'apple::Apple M5 Max', name: 'Apple M5 Max', vendor: 'apple', kind: 'apple', worker: 'Metal', vram_mb: 131072 }), + }; + for (const [v, cd] of Object.entries(rows)) { + const mk = V.vendorMark(cd); + assert.equal(mk.vendor, v); + assert.match(mk.svg, /^ { + const mali = card({ key: 'other:0:Mali', name: 'Mali-G78 MP20', vendor: 'other', kind: 'integrated', worker: 'OpenCL', vram_mb: 0 }); + const mk = V.vendorMark(mali); + assert.equal(mk.vendor, 'gpu'); assert.equal(mk.label, 'GPU'); assert.equal(mk.svg, V.MARKS.gpu); + assert.ok(V.markHtml(mali).startsWith('
    { + const amd = card({ key: 'amd:gfx1201', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', vendor: 'amd' }); + assert.equal(marks(V.rowHead(amd)), 1); + assert.equal(marks(V.rowHead(amd, ': removed')), 1); + assert.equal(marks(V.markHtml(amd) + V.nameHtml(amd)), 1); + assert.equal(marks(V.nameHtml(amd)), 0, 'the name block carries no mark of its own'); + // the renderers: the first-run rows, the Cards rows and the block card all go through View.markHtml; no other + // string in app.js opens a badge + assert.equal((src.match(/class="badge /g) || []).length, 1, 'one place in app.js opens a badge'); + assert.equal((src.match(/View\.markHtml\(/g) || []).length, 3, 'first-run rows, Cards rows, the block card'); + assert.equal(src.includes('badgeHtml('), false); +}); + +test('the series line under the name is mono text per generation; an integrated row (Intel iGPU, Apple) ends in "integrated" and keeps one mark', () => { + assert.equal(V.seriesLine(card()), 'RTX 50 series · Blackwell'); + assert.equal(V.seriesLine(card({ name: 'NVIDIA GeForce RTX 4070' })), 'RTX 40 series · Ada Lovelace'); + assert.equal(V.seriesLine(card({ name: 'NVIDIA GeForce RTX 3080' })), 'RTX 30 series · Ampere'); + assert.equal(V.seriesLine(card({ name: 'NVIDIA GeForce GTX 1660 Super' })), 'GTX 16 series · Turing'); + assert.equal(V.seriesLine(card({ name: 'AMD Radeon RX 9070 XT', vendor: 'amd' })), 'RX 9000 series · RDNA 4'); + assert.equal(V.seriesLine(card({ name: 'AMD Radeon RX 7800 XT', vendor: 'amd' })), 'RX 7000 series · RDNA 3'); + assert.equal(V.seriesLine(card({ name: 'gfx1201', code: 'gfx1201', vendor: 'amd' })), 'RDNA 4'); + assert.equal(V.seriesLine(card({ name: 'Intel(R) Arc(TM) B580 Graphics', vendor: 'intel' })), 'Arc B series · Battlemage'); + assert.equal(V.seriesLine(card({ name: 'Intel Arc A770', vendor: 'intel' })), 'Arc A series · Alchemist'); + assert.equal(V.seriesLine(card({ name: 'Intel UHD Graphics 770', vendor: 'other', kind: 'integrated' })), 'UHD Graphics · integrated'); + assert.equal(V.seriesLine(card({ name: 'Intel Iris Xe Graphics', vendor: 'other', kind: 'integrated' })), 'Iris Xe · integrated'); + assert.equal(V.seriesLine(card({ name: 'AMD Radeon(TM) Graphics', vendor: 'amd', kind: 'integrated' })), 'Radeon · integrated'); + assert.equal(V.seriesLine(card({ name: 'Apple M5 Max', vendor: 'apple', kind: 'apple' })), 'M5 series · integrated'); + const igpu = card({ name: 'Intel UHD Graphics 770', vendor: 'other', kind: 'integrated' }); + const head = V.rowHead(igpu); + assert.equal(marks(head), 1); assert.match(head, /data-mark="intel"/); assert.match(head, /
    UHD Graphics · integrated<\/div>/); + assert.equal(V.vendorMark(igpu).svg, V.MARKS.intel, 'an integrated Intel row keeps the Intel glyph, never a second mark'); + assert.equal(V.nameHtml(card({ name: 'x', vendor: 'zzz', kind: 'unknown' })).includes(''), false, 'the name is escaped'); +}); + +test('the light theme contrast ratio of every vendor colour on its well is at least 3:1, and app.css carries the same hex values and wells', () => { + for (const v of Object.keys(V.VENDORS)) { + const light = V.vendorContrast(v, 'light'), dark = V.vendorContrast(v, 'dark'); + assert.ok(light >= 3, v + ' light: ' + light.toFixed(2) + ':1 on its well'); + assert.ok(dark >= 3, v + ' dark: ' + dark.toFixed(2) + ':1 on its well'); + const [r, g, b] = V.hexRgb(V.VENDORS[v].light), [dr, dg, db] = V.hexRgb(V.VENDORS[v].dark); + assert.ok(css.includes('--mark-' + v + ':' + V.VENDORS[v].dark + ';'), v + ' dark hex in app.css'); + assert.equal((css.match(new RegExp('--mark-' + v + ':' + V.VENDORS[v].light + ';', 'g')) || []).length, 2, v + ' light hex in both light blocks'); + assert.ok(css.includes('--mark-' + v + '-well:rgba(' + dr + ',' + dg + ',' + db + ',' + String(V.WELL_ALPHA_DARK).replace(/^0/, '') + ')'), v + ' dark well'); + assert.equal((css.match(new RegExp('--mark-' + v + '-well:rgba\\(' + r + ',' + g + ',' + b + ',' + String(V.WELL_ALPHA_LIGHT).replace(/^0/, '') + '\\)', 'g')) || []).length, 2, v + ' light well in both light blocks'); + } + // the ember accent is for state, never a brand + for (const v of Object.keys(V.VENDORS)) { assert.notEqual(V.VENDORS[v].dark, '#F2541B'); assert.notEqual(V.VENDORS[v].light, '#E04A14'); } + assert.equal(/\.badge\.(nvidia|amd|intel|apple|gpu)\{[^}]*var\(--ember/.test(css), false, 'no badge rule tints with ember'); + assert.equal(/\.badge[^{]*\{[^}]*animation/.test(css), false, 'nothing on the mark animates'); +}); + +// ---------- the custom switch (the Prove page fix, 7 October 2026) ---------- +// the project lead's screenshot from the live 0.3.19 Mac app: a thin white rectangle above-left of the "Prove on this machine" +// switch and the knob drawn outside the track's left edge at off. Two generic classes reached the switch (the DAG +// legend swatch .lg hit label.switch.lg, the shard track .track hit .switch .track) and the native input was an +// unpositioned absolute. This test reads app.css: no bare .lg or .track rule may exist (every use is scoped), the +// native input is hidden the accessible way (1 px clip, still focusable), and the knob's box sits inside the track's +// box at off and at on for both sizes, in the base rules and untouched by both theme blocks. +const cssRules = (text) => { const out = []; const re = /([^{}]+)\{([^{}]*)\}/g; let m; text = text.replace(/\/\*[\s\S]*?\*\//g, ''); while ((m = re.exec(text))) out.push({ sel: m[1].trim(), body: m[2] }); return out; }; +const ruleOf = (sel) => cssRules(css).filter((r) => r.sel.split(',').map((x) => x.trim()).includes(sel)).map((r) => r.body).join(';'); +const px = (body, prop) => { const m = new RegExp('(?:^|;)\\s*' + prop + ':\\s*(-?[\\d.]+)px').exec(body); return m ? parseFloat(m[1]) : null; }; +const translate = (body) => { const m = /transform:\s*translateX\((-?[\d.]+)px\)/.exec(body); return m ? parseFloat(m[1]) : 0; }; + +test('no generic .lg or .track rule reaches a custom switch: every use of those class names is scoped (known-failed on the 0.3.19 CSS)', () => { + const bare = cssRules(css).flatMap((r) => r.sel.split(',').map((x) => x.trim())).filter((sel) => /^\.(lg|track)(\.[\w-]+)*(\s+[\w.:-]+)*$/.test(sel) && !/^\.switch\b/.test(sel)); + assert.deepEqual(bare, [], 'selectors that start with a bare .lg or .track: ' + bare.join(' | ')); + assert.equal(/