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 <noreply@anthropic.com>
This commit is contained in:
parent
fa8939ee48
commit
d1e42080f8
5 changed files with 161 additions and 15 deletions
16
site/lib/money-regen.mjs
Normal file
16
site/lib/money-regen.mjs
Normal file
|
|
@ -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');
|
||||
48
site/lib/money.test.mjs
Normal file
48
site/lib/money.test.mjs
Normal file
|
|
@ -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);
|
||||
});
|
||||
61
site/money.js
Normal file
61
site/money.js
Normal file
|
|
@ -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);
|
||||
|
|
@ -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 = '<span class="badge-slot"></span><b>Your card</b><select aria-label="Your card">' + CARDS.map(function (c) { return '<option value="' + c[1] + '"' + (chosen === c[0] ? ' selected' : '') + '>' + c[0] + ' \u00b7 ' + c[1] + ' MH/s</option>'; }).join('') + '</select><span>finds a block about <span class="eta">pending</span> at the network\u2019s hash rate right now.</span>';
|
||||
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 = '<b>Your card</b><select class="card" aria-label="Your card">' + CARDS.map(function (c) { return '<option value="' + c[1] + '"' + (chosen === c[0] ? ' selected' : '') + '>' + c[0] + ' \u00b7 ' + c[1] + ' MH/s</option>'; }).join('') + '</select><span>finds a block about <span class="eta">pending</span> at the network\u2019s hash rate right now.</span>' +
|
||||
(M ? '<span class="cost" style="display:block;margin-top:4px"></span><select class="cur" aria-label="Currency" style="margin-left:8px">' + M.CURRENCY_CODES.map(function (c) { return '<option value="' + c + '"' + (ch.code === c ? ' selected' : '') + '>' + c + '</option>'; }).join('') + '</select>' : '');
|
||||
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(); } } };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue