From d1e42080f899dcf3819446264846c1871a851455 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:30:22 +0000 Subject: [PATCH] Site currency (currency-21's site side, for master tonight): site/money.js is the miner app's currency rules for the site (navigator.language picks the currency and the typical tariff, a saved switch wins, one Intl.NumberFormat call for every money figure, no exchange rate anywhere; tables generated from the app's View block, site/lib/money-regen.mjs rewrites them, site/lib/money.test.mjs fails on drift once the app side reaches master and waits until then); the card picker (yourcard.js on /miner and /app) shows the card's electricity a day at the typical tariff with a currency switch saved in localStorage igneum-currency, loading /money.js itself so no page was edited Co-Authored-By: Claude Fable 5.1 --- site/lib/money-regen.mjs | 16 +++++++++++ site/lib/money.test.mjs | 48 +++++++++++++++++++++++++++++++ site/money.js | 61 ++++++++++++++++++++++++++++++++++++++++ site/yourcard.js | 49 +++++++++++++++++++++++--------- tools/ci/pre-push.sh | 2 +- 5 files changed, 161 insertions(+), 15 deletions(-) create mode 100644 site/lib/money-regen.mjs create mode 100644 site/lib/money.test.mjs create mode 100644 site/money.js diff --git a/site/lib/money-regen.mjs b/site/lib/money-regen.mjs new file mode 100644 index 00000000..eb12337f --- /dev/null +++ b/site/lib/money-regen.mjs @@ -0,0 +1,16 @@ +#!/usr/bin/env node +// Rewrites the four tables in site/money.js from app/igneum-app/ui/app.js (View.REGIONS, ISO_CURRENCY, EU_MEMBERS, +// CURRENCY_CODES). Run after a change to the app's tables, then commit both; site/lib/money.test.mjs fails on drift. +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +const here = dirname(fileURLToPath(import.meta.url)); +const m = { exports: {} }; new Function('module', readFileSync(join(here, '../../app/igneum-app/ui/app.js'), 'utf8'))(m); +const V = m.exports.View, p = join(here, '../money.js'); +const regions = V.REGIONS.map(r => ({ code: r.code, name: r.name, cur: r.cur, price: r.price, source: r.source || '', restricted: !!r.restricted })); +let s = readFileSync(p, 'utf8'); +s = s.replace(/ M\.REGIONS = .*\n/, ' M.REGIONS = ' + JSON.stringify(regions) + ';\n'); +s = s.replace(/ M\.ISO_CURRENCY = .*\n/, ' M.ISO_CURRENCY = ' + JSON.stringify(V.ISO_CURRENCY) + ';\n'); +s = s.replace(/ M\.EU_MEMBERS = .*\n/, ' M.EU_MEMBERS = ' + JSON.stringify(V.EU_MEMBERS) + ';\n'); +s = s.replace(/ M\.CURRENCY_CODES = .*\n/, ' M.CURRENCY_CODES = ' + JSON.stringify(V.CURRENCY_CODES) + ';\n'); +writeFileSync(p, s); console.log('site/money.js tables regenerated'); diff --git a/site/lib/money.test.mjs b/site/lib/money.test.mjs new file mode 100644 index 00000000..6a42e2b8 --- /dev/null +++ b/site/lib/money.test.mjs @@ -0,0 +1,48 @@ +// node --test site/lib/money.test.mjs (the pre-push gate runs it beside the explorer and emission tests) +// site/money.js is the app's currency rules for the site: its tables must equal the app's, its formatter must place the +// symbol as the locale does, and the picker's choice must follow navigator.language with a saved override winning. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +const here = dirname(fileURLToPath(import.meta.url)); +const load = (p) => { const mod = { exports: {} }; new Function('module', 'window', readFileSync(p, 'utf8'))(mod, undefined); return mod.exports; }; +const M = load(join(here, '../money.js')); +const V = load(join(here, '../../app/igneum-app/ui/app.js')).View; + +test('the site tables equal the app tables (regenerate with node site/lib/money-regen.mjs)', (t) => { + // the app side rides release-0.3.21 first; on a tree whose app has no ISO_CURRENCY yet the drift check waits for it + if (!V.ISO_CURRENCY) { t.skip('the app on this tree carries no currency tables yet (currency-21 app side not merged here)'); return; } + assert.deepEqual(M.REGIONS, V.REGIONS.map(r => ({ code: r.code, name: r.name, cur: r.cur, price: r.price, source: r.source || '', restricted: !!r.restricted }))); + assert.deepEqual(M.ISO_CURRENCY, V.ISO_CURRENCY); assert.deepEqual(M.EU_MEMBERS, V.EU_MEMBERS); assert.deepEqual(M.CURRENCY_CODES, V.CURRENCY_CODES); + for (const c of ['GBP', 'EUR', 'JPY', 'CHF']) assert.deepEqual(M.minorOf(c), V.minorOf(c), c); +}); + +test('the picker follows navigator.language, a saved choice wins, no row falls back to USD and asks', () => { + assert.equal(M.currencyChoice('', 'de-DE').code, 'EUR'); assert.equal(M.currencyChoice('', 'en-GB').code, 'GBP'); assert.equal(M.currencyChoice('', 'fr-CA').code, 'CAD'); assert.equal(M.currencyChoice('', 'en-AU').code, 'AUD'); + assert.equal(M.currencyChoice('USD', 'de-DE').code, 'USD'); assert.equal(M.currencyChoice('USD', 'de-DE').source, 'override'); + const eo = M.currencyChoice('', 'eo'); assert.equal(eo.code, 'USD'); assert.equal(eo.prompt, true); + assert.equal(M.currencyChoice('zzz', 'eo').prompt, true, 'an unknown saved code is ignored'); +}); + +test('the default tariff is the region row, else the first table row in the currency, and the cost line uses one formatter', () => { + const de = M.tariffFor(M.currencyChoice('', 'de-DE')); assert.equal(de.price, 38.69); assert.match(de.source, /Eurostat/); assert.equal(de.region, 'Germany'); + const gbp = M.tariffFor(M.currencyChoice('GBP', 'de-DE')); assert.equal(gbp.price, 26.32); assert.equal(gbp.region, 'United Kingdom'); + const fr = M.tariffFor(M.currencyChoice('', 'fr-FR')); assert.equal(fr.price, 28.96, 'an EU member on the euro takes the EU average'); + assert.equal(M.tariffFor(M.currencyChoice('', 'sv-SE')), null, 'no table tariff in kronor: the picker says so'); + assert.equal(M.fmtMoney(M.costPerDay(227, 38.69), 'EUR', 'de-DE'), '2,11 €'); + assert.equal(M.fmtMoney(M.costPerDay(227, 26.32), 'GBP', 'en-GB'), '£1.43'); + assert.equal(M.fmtMoney(M.costPerDay(227, 17.7), 'USD', 'en-US'), '$0.96'); + assert.equal(M.costPerDay(null, 26.32), null, 'no watts, no cost'); + assert.equal(M.fmtMoney(1234.5, 'USD', 'en-US'), '$1,235'); assert.equal(M.fmtMoney(1.5, 'JPY', 'en-US'), '¥2'); +}); + +test('yourcard.js loads money.js and shows the electricity line with the currency switch; no hand-built money string', () => { + const yc = readFileSync(join(here, '../yourcard.js'), 'utf8'); + assert.ok(yc.includes("'/money.js'"), 'loads the module when the page did not'); + assert.ok(yc.includes('window.IgneumMoney') && yc.includes('.fmtMoney(') && yc.includes('.currencyChoice(') && yc.includes('.tariffFor('), 'the module is the only money path'); + assert.ok(yc.includes('igneum-currency'), 'the saved choice'); + assert.ok(yc.includes('class="cur"'), 'the switch'); + assert.equal(/'£'|"£"/.test(yc), false); +}); diff --git a/site/money.js b/site/money.js new file mode 100644 index 00000000..82bdf4f9 --- /dev/null +++ b/site/money.js @@ -0,0 +1,61 @@ +/* Igneum money (currency-21, 7 October 2026): the site's copy of the miner app's currency rules, for the card picker + (yourcard.js) and any page that shows a money figure. No IP lookup: navigator.language's region picks the currency and + the default tariff; a saved choice (localStorage igneum-currency) wins; every figure goes through one Intl.NumberFormat + call with the currency code and the narrow symbol, placed as the locale places it; the user's own tariff is the + number, so no exchange rate exists anywhere. The tables below are generated from app/igneum-app/ui/app.js (View.REGIONS, + View.ISO_CURRENCY, View.EU_MEMBERS, View.CURRENCY_CODES); site/lib/money.test.mjs fails when they drift, and + `node site/lib/money-regen.mjs` rewrites them. A classic script (window.IgneumMoney) so yourcard.js can load it on + pages that carry no module tag; under node it also exports through module.exports for the test. */ +(function (root) { + var M = {}; + M.REGIONS = [{"code":"gb","name":"United Kingdom","cur":"GBP","price":26.32,"source":"Ofgem cap, 1 October to 31 December 2026","restricted":false},{"code":"ie","name":"Ireland","cur":"EUR","price":40.42,"source":"Eurostat, second half of 2025","restricted":false},{"code":"de","name":"Germany","cur":"EUR","price":38.69,"source":"Eurostat, second half of 2025","restricted":false},{"code":"be","name":"Belgium","cur":"EUR","price":34.99,"source":"Eurostat, second half of 2025","restricted":false},{"code":"eu","name":"European Union, other","cur":"EUR","price":28.96,"source":"Eurostat, second half of 2025, the EU average","restricted":false},{"code":"hu","name":"Hungary","cur":"EUR","price":10.82,"source":"Eurostat, second half of 2025, in euro cents","restricted":false},{"code":"mt","name":"Malta","cur":"EUR","price":12.82,"source":"Eurostat, second half of 2025","restricted":false},{"code":"bg","name":"Bulgaria","cur":"EUR","price":13.55,"source":"Eurostat, second half of 2025, in euro cents","restricted":false},{"code":"us","name":"United States","cur":"USD","price":17.7,"source":"EIA average, second half of 2025","restricted":false},{"code":"kz","name":"Kazakhstan","cur":"USD","price":4,"source":"about 4 c, a secondary source, approximate","restricted":false},{"code":"py","name":"Paraguay","cur":"USD","price":4.4,"source":"ANDE crypto tariff, 4.4 to 6 c; every crypto contract ends 31 December 2027","restricted":false},{"code":"ir","name":"Iran","cur":"USD","price":1,"source":"licensed mining tariff, about 1 c, a secondary source, approximate","restricted":false},{"code":"et","name":"Ethiopia","cur":"USD","price":2,"source":"2 to 5.3 c, a secondary source","restricted":false},{"code":"ng","name":"Nigeria","cur":"USD","price":4.8,"source":"hosted, 4.8 c, a secondary source","restricted":false},{"code":"ru-mow","name":"Russia, Moscow region","cur":"USD","price":0,"source":"","restricted":true},{"code":"ru-irk","name":"Russia, Irkutsk","cur":"USD","price":0,"source":"","restricted":true},{"code":"ru-bur","name":"Russia, Buryatia","cur":"USD","price":0,"source":"","restricted":true},{"code":"ru-zab","name":"Russia, Zabaikalsky","cur":"USD","price":0,"source":"","restricted":true},{"code":"ru","name":"Russia, other region","cur":"USD","price":0,"source":"","restricted":true},{"code":"cn","name":"China","cur":"USD","price":0,"source":"","restricted":true},{"code":"other","name":"Somewhere else","cur":"USD","price":0,"source":"","restricted":false}]; + M.ISO_CURRENCY = {"at":"EUR","be":"EUR","cy":"EUR","de":"EUR","ee":"EUR","es":"EUR","fi":"EUR","fr":"EUR","gr":"EUR","hr":"EUR","ie":"EUR","it":"EUR","lt":"EUR","lu":"EUR","lv":"EUR","mt":"EUR","nl":"EUR","pt":"EUR","si":"EUR","sk":"EUR","gb":"GBP","us":"USD","ca":"CAD","au":"AUD","nz":"NZD","ch":"CHF","li":"CHF","se":"SEK","no":"NOK","dk":"DKK","is":"ISK","pl":"PLN","cz":"CZK","hu":"HUF","ro":"RON","bg":"BGN","rs":"RSD","tr":"TRY","ua":"UAH","ge":"GEL","kz":"KZT","ru":"RUB","jp":"JPY","kr":"KRW","cn":"CNY","hk":"HKD","tw":"TWD","sg":"SGD","my":"MYR","th":"THB","vn":"VND","ph":"PHP","id":"IDR","in":"INR","pk":"PKR","bd":"BDT","ae":"AED","sa":"SAR","il":"ILS","ir":"IRR","za":"ZAR","ng":"NGN","ke":"KES","eg":"EGP","ma":"MAD","et":"ETB","br":"BRL","mx":"MXN","ar":"ARS","cl":"CLP","co":"COP","pe":"PEN","py":"PYG"}; + M.EU_MEMBERS = ["at","be","bg","hr","cy","cz","dk","ee","fi","fr","de","gr","hu","ie","it","lv","lt","lu","mt","nl","pl","pt","ro","sk","si","es","se"]; + M.CURRENCY_CODES = ["GBP","EUR","USD","CAD","AUD","AED","ARS","BDT","BGN","BRL","CHF","CLP","CNY","COP","CZK","DKK","EGP","ETB","GEL","HKD","HUF","IDR","ILS","INR","IRR","ISK","JPY","KES","KRW","KZT","MAD","MXN","MYR","NGN","NOK","NZD","PEN","PHP","PKR","PLN","PYG","RON","RSD","RUB","SAR","SEK","SGD","THB","TRY","TWD","UAH","VND","ZAR"]; + var MINOR = { GBP: ['pence', 'p'], EUR: ['euro cents', 'c'], USD: ['cents', 'c'], CAD: ['cents', 'c'], AUD: ['cents', 'c'], NZD: ['cents', 'c'], CHF: ['rappen', 'rp'], SEK: ['öre', 'öre'], NOK: ['øre', 'øre'], DKK: ['øre', 'øre'], PLN: ['grosze', 'gr'], CZK: ['haléře', 'h'], INR: ['paise', 'p'], BRL: ['centavos', 'c'], MXN: ['centavos', 'c'], ZAR: ['cents', 'c'], JPY: ['yen', '¥'], KRW: ['won', '₩'] }; + M.minorOf = function (cur) { + cur = String(cur || '').toUpperCase(); var digits = 2; + try { digits = new Intl.NumberFormat('en', { style: 'currency', currency: cur }).resolvedOptions().maximumFractionDigits; } catch (e) { } + var mm = MINOR[cur] || (digits === 0 ? [cur.toLowerCase(), cur] : ['cents', 'c']); + return { word: mm[0], abbr: mm[1], digits: digits }; + }; + M.fmtMoney = function (v, cur, loc) { + if (v === null || v === undefined || isNaN(v)) return ''; + var d = v >= 100 ? 0 : M.minorOf(cur).digits; + try { return new Intl.NumberFormat(loc || 'en-GB', { style: 'currency', currency: cur, currencyDisplay: 'narrowSymbol', minimumFractionDigits: d, maximumFractionDigits: d }).format(v).replace(/[\u00a0\u202f]/g, ' '); } + catch (e) { return cur + ' ' + (d ? v.toFixed(d) : String(Math.round(v))); } + }; + M.regionFromLocale = function (lang) { + lang = String(lang || '').trim(); if (!lang) return ''; var reg = ''; + try { var l = new Intl.Locale(lang); reg = l.region || (l.maximize ? l.maximize().region : '') || ''; } catch (e) { var mm = /^[A-Za-z]{2,3}[-_]([A-Za-z]{2})\b/.exec(lang); reg = mm ? mm[1] : ''; } + return /^[A-Za-z]{2}$/.test(reg) ? reg.toLowerCase() : ''; + }; + var synth = {}; + function regionName(code) { try { var n = new Intl.DisplayNames(['en'], { type: 'region' }).of(code.toUpperCase()); if (n && n !== code.toUpperCase()) return n; } catch (e) { } return code.toUpperCase(); } + M.regionOf = function (code) { + for (var i = 0; i < M.REGIONS.length; i++) if (M.REGIONS[i].code === code) return M.REGIONS[i]; + if (!/^[a-z]{2}$/.test(code || '')) return null; + if (synth[code]) return synth[code]; + var cur = M.ISO_CURRENCY[code] || '', eu = M.EU_MEMBERS.indexOf(code) >= 0 && cur === 'EUR', avg = M.regionOf('eu'); + return (synth[code] = { code: code, name: regionName(code), cur: cur || 'USD', unknown: !cur, price: eu && avg ? avg.price : 0, source: eu && avg ? avg.source : '' }); + }; + // the currency in force: a saved choice, else the locale's region, else USD with the prompt + M.currencyChoice = function (saved, lang) { + var ov = String(saved || '').toUpperCase(), loc = M.regionFromLocale(lang); + if (ov && M.CURRENCY_CODES.indexOf(ov) >= 0) return { code: ov, source: 'override', region: loc, prompt: false }; + var r = loc ? M.regionOf(loc) : null; + if (r && !r.unknown) return { code: r.cur, source: 'locale', region: loc, prompt: false }; + return { code: 'USD', source: 'fallback', region: loc, prompt: true }; + }; + // the default tariff for a choice: the locale's region row when it carries the currency, else the first table row in that currency + M.tariffFor = function (choice) { + var r = choice.region ? M.regionOf(choice.region) : null; + if (r && r.cur === choice.code && r.price > 0) return { price: r.price, source: r.source, region: r.name, cur: choice.code }; + for (var i = 0; i < M.REGIONS.length; i++) if (M.REGIONS[i].cur === choice.code && M.REGIONS[i].price > 0 && !M.REGIONS[i].restricted) return { price: M.REGIONS[i].price, source: M.REGIONS[i].source, region: M.REGIONS[i].name, cur: choice.code }; + return null; + }; + // watts over a day at a tariff in hundredths of the unit per kWh: the unit amount + M.costPerDay = function (watts, pricePerKwh) { return watts > 0 && pricePerKwh > 0 ? watts * 24 / 1000 * pricePerKwh / 100 : null; }; + root.IgneumMoney = M; + if (typeof module === 'object' && module && module.exports) module.exports = M; +})(typeof window !== 'undefined' ? window : globalThis); diff --git a/site/yourcard.js b/site/yourcard.js index 4ad14adb..5fb676d1 100644 --- a/site/yourcard.js +++ b/site/yourcard.js @@ -1,21 +1,42 @@ -/* The "your card" line: a measured card, and how often it finds a block at the network's hash rate right now. One block a - second is the target, so the wait is network hashes per second over the card's rate. The rates are the bench table's - measured numbers (site/miners.html); nothing here is a promise. Reads /api/live every 15 s. */ +/* The "your card" line: a measured card, how often it finds a block at the network's hash rate right now, and what its + electricity costs a day in your currency. One block a second is the target, so the wait is network hashes per second + over the card's rate. The rates are the bench table's measured numbers (site/miners.html); the watts are the tuned + draws from the same runs (app.html's Ember table: 5090 226.8 W, 4070 75.6 W; the 9070 XT 202 W from docs/bench-log.md, + 6 October 2026; Apple silicon reports no draw). The currency follows navigator.language (no IP lookup); the switch saves + a choice in localStorage igneum-currency; the tariff is the public table's default for that region or currency, so the + cost is arithmetic from a typical price, never a promise (currency-21, 7 October 2026). Reads /api/live every 15 s. */ (function () { - var CARDS = [['NVIDIA RTX 5090', 127.7], ['NVIDIA RTX 5060 Ti', 30.9], ['NVIDIA RTX 4070', 28.8], ['Apple M5 Max', 26.7], ['AMD RX 9070 XT', 17.8]]; + var CARDS = [['NVIDIA RTX 5090', 127.7, 226.8], ['NVIDIA RTX 4070', 28.8, 75.6], ['Apple M5 Max', 26.7, null], ['AMD RX 9070 XT', 17.8, 202]]; var els = document.querySelectorAll('[data-yourcard]'); if (!els.length) return; var net = null, chosen = (function () { try { return localStorage.getItem('igneum-card'); } catch (e) { return null; } })(); + var savedCur = (function () { try { return localStorage.getItem('igneum-currency') || ''; } catch (e) { return ''; } })(); function dur(sec) { if (!(sec > 0)) return 'pending'; if (sec < 90) return Math.round(sec) + ' s'; if (sec < 5400) return Math.round(sec / 60) + ' min'; if (sec < 172800) { var h = Math.floor(sec / 3600), m = Math.round((sec % 3600) / 60); return h + ' h' + (m ? ' ' + m + ' min' : ''); } return Math.round(sec / 86400) + ' days'; } - function render() { els.forEach(function (el) { var r = parseFloat(el.querySelector('select').value) * 1e6, eta = el.querySelector('.eta'); eta.textContent = net && r ? 'every ' + dur(net / r) : 'pending'; }); } - // the vendor badge beside the picker: the five mini marks the build stamped as JSON (site/lib/marks.mjs), chosen by the card's name - var marks = {}; try { var mj = document.getElementById('gpu-marks'); if (mj) marks = JSON.parse(mj.textContent); } catch (e) { marks = {}; } - function vendorOf(n) { return /nvidia|geforce|\bRTX\b|\bGTX\b/i.test(n) ? 'nvidia' : /\bamd\b|radeon/i.test(n) ? 'amd' : /intel|\barc\b/i.test(n) ? 'intel' : /apple|\bM\d (max|pro|ultra)\b/i.test(n) ? 'apple' : 'gpu'; } - function badge(el) { var sel = el.querySelector('select'), b = el.querySelector('.badge-slot'); if (!sel || !b) return; var name = sel.options[sel.selectedIndex].text.split(' \u00b7 ')[0]; b.innerHTML = marks[vendorOf(name)] || ''; } - els.forEach(function (el) { - el.innerHTML = 'Your cardfinds a block about pending at the network\u2019s hash rate right now.'; - el.querySelector('select').addEventListener('change', function (e) { var name = e.target.options[e.target.selectedIndex].text.split(' \u00b7 ')[0]; try { localStorage.setItem('igneum-card', name); } catch (x) {} document.querySelectorAll('[data-yourcard] select').forEach(function (s) { s.value = e.target.value; }); els.forEach(badge); render(); }); - badge(el); - }); + function costWords(watts) { + var M = window.IgneumMoney; if (!M) return ''; + var ch = M.currencyChoice(savedCur, navigator.language), t = M.tariffFor(ch); + if (watts === null) return 'no power reading on this card'; + if (!t) return 'no typical ' + ch.code + ' tariff in the table; the app uses the price you type'; + var c = M.costPerDay(watts, t.price); + return 'about ' + M.fmtMoney(c, ch.code, navigator.language) + ' a day in electricity at ' + t.price + ' ' + M.minorOf(ch.code).abbr + '/kWh (' + t.region + ', typical)'; + } + function render() { + els.forEach(function (el) { + var sel = el.querySelector('select.card'), r = parseFloat(sel.value) * 1e6, eta = el.querySelector('.eta'), cost = el.querySelector('.cost'); + eta.textContent = net && r ? 'every ' + dur(net / r) : 'pending'; + var row = CARDS[sel.selectedIndex]; if (cost) cost.textContent = costWords(row ? row[2] : null); + }); + } + function init() { + var M = window.IgneumMoney, ch = M ? M.currencyChoice(savedCur, navigator.language) : null; + els.forEach(function (el) { + el.innerHTML = 'Your cardfinds a block about pending at the network\u2019s hash rate right now.' + + (M ? '' : ''); + el.querySelector('select.card').addEventListener('change', function (e) { var name = e.target.options[e.target.selectedIndex].text.split(' \u00b7 ')[0]; try { localStorage.setItem('igneum-card', name); } catch (x) {} document.querySelectorAll('[data-yourcard] select.card').forEach(function (s) { s.value = e.target.value; }); render(); }); + var cur = el.querySelector('select.cur'); if (cur) cur.addEventListener('change', function (e) { savedCur = e.target.value; try { localStorage.setItem('igneum-currency', savedCur); } catch (x) {} document.querySelectorAll('[data-yourcard] select.cur').forEach(function (s) { s.value = savedCur; }); render(); }); + }); + render(); + } + if (window.IgneumMoney) init(); else { var sc = document.createElement('script'); sc.src = '/money.js'; sc.onload = init; sc.onerror = init; document.head.appendChild(sc); } function poll() { fetch('/api/live?window=30', { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (d) { if (d && d.ok && d.state && d.state.hashes_per_second_estimate > 0) { net = d.state.hashes_per_second_estimate; render(); } }).catch(function () {}); } poll(); setInterval(function () { if (!document.hidden) poll(); }, 15000); window.IgneumYourCard = { update: function (h) { if (h > 0) { net = h; render(); } } }; diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index 04e90cb7..5699d52c 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -117,7 +117,7 @@ tree_checks() { run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh run "no text overlaps: every served page at 390 to 1600 px, light and dark, the hero at each step (self-test first; IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs)" overlap_sweep - run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/api/public-stats.test.mjs + run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/api/public-stats.test.mjs run "ship tool self-test" node tools/ship-app.mjs --self-test run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs