View: poisson, ignPerDay, ladderRungs (every threshold from the node's params: dust, weightWindow, presenceWindow),
blockCard, earningsLines (IGN first, the typed price never fetched, the share and rank, IGN per kWh), cardIgnDay,
timeline, profileWords; FIELDS names every number's RPC field for the title on hover. Overview: the Poisson
count-up until the first block, the block card in place with Save the card (a canvas PNG, no network call), Copy the
link and the explorer, the ladder strip. Earnings: the six lines, the ladder card, Your first hour. Prove: the shard
card. Cards: IGN a day per card. Settings: Make my page public. The chain scene is the site lane's EMBER 02 pack
(live-dag.js and proof-core.js byte-identical): the compact card fed by the page's own api/live read, Inspect opens
the full scene with the block inspector in the site's words. ui-mock: scenarios firstwait, firstblock, ladder,
api/ladder, api/card. view.test.mjs: a Devnet 2 key walks every rung; 47 UI tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 32793b686a)
249 lines
22 KiB
JavaScript
249 lines
22 KiB
JavaScript
#!/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
|
|
// firstwait miner-ui-5: mining, no block yet (the Poisson count-up), the ladder's first rung
|
|
// firstblock miner-ui-5: the first block a minute ago (the block card), the key under the dust line
|
|
// ladder miner-ui-5: every rung walked (vote, signature, full window, rank 3, streak, a paid shard)
|
|
// GET /api/ladder answers the chain facts in src/chainfacts.rs's shape (the devnet's params, this key ranked);
|
|
// POST /api/card accepts the block card's PNG and answers a path.
|
|
// 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';
|
|
|
|
// IGNEUM_MOCK_STATE=<file> replays a state captured from a real machine (relay/playbooks/app-state-grab.ps1) instead of
|
|
// the recorded one; IGNEUM_MOCK_LIVE=<file> serves that machine's /api/live reply as it was (timestamps shifted to now)
|
|
const recState = JSON.parse(fs.readFileSync(process.env.IGNEUM_MOCK_STATE || path.join(here, 'recorded/state.json'), 'utf8'));
|
|
const recLive = process.env.IGNEUM_MOCK_LIVE ? JSON.parse(fs.readFileSync(process.env.IGNEUM_MOCK_LIVE, 'utf8')) : null;
|
|
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);
|
|
|
|
// /api/live for the embedded DAG scene (site/live-dag.js): a synthetic chain at one block a second, four miner lanes
|
|
// ("you" is this machine), parents into the last two seconds, the selected chain through "you" and "miner-2", blue
|
|
// and red colouring, proofs filling two blocks in three after 20 s, a checkpoint band every 30 s locked after 10 s.
|
|
const LIVE_T0 = Math.floor(Date.now() / 1000) - 400;
|
|
function liveNow(windowS) {
|
|
const t = Math.floor(now()), from = t - Math.min(300, Math.max(30, windowS)) - 25, blocks = [];
|
|
const lanes = ['you', 'miner-2', 'miner-3', 'miner-4'];
|
|
let prev = [];
|
|
for (let s = from; s <= t - 2; s++) {
|
|
const seed = ((s % 100000) * 7919 + ((s >> 3) % 97) * 131) % 1000;
|
|
const miner = lanes[seed % 4], hash = 'b' + s.toString(16).padStart(10, '0');
|
|
const chain = miner === 'you' || miner === 'miner-2' ? (seed % 7 !== 3) : (seed % 5 === 0);
|
|
const color = seed % 11 === 0 ? 'red' : (s < t - 4 ? 'blue' : 'pending');
|
|
const proven = color === 'blue' && s < t - 20 && seed % 3 !== 0;
|
|
const shards = proven ? [{ state: 'paid' }] : color === 'blue' && s < t - 8 ? [{ state: 'proving' }] : [];
|
|
const parents = prev.slice(-2).map(b => b.hash);
|
|
const locked = (s - LIVE_T0) % 30 === 0 && s < t - 10;
|
|
const b = { hash, ts: s * 1000, blue_score: 194000 + (s - LIVE_T0), daa: 198000 + (s - LIVE_T0), parents, chain, color, miner, locked, final: s < t - 40, shards, proven };
|
|
blocks.push(b); prev.push(b); if (prev.length > 3) prev.shift();
|
|
}
|
|
const checkpoints = [];
|
|
for (let s = from; s <= t - 2; s++) if ((s - LIVE_T0) % 30 === 0) checkpoints.push({ index: Math.floor((s - LIVE_T0) / 30), hash: 'c' + s.toString(16), blue_score: 194000 + (s - LIVE_T0), daa: 198000 + (s - LIVE_T0), state: s < t - 10 ? 'locked' : 'pending', fraction_total: s < t - 10 ? 0.71 : 0.44 });
|
|
return { ok: true, now: t, state: { stale: false, age_s: 1, height: 198000 + (t - LIVE_T0) }, blocks, finality: { checkpoints } };
|
|
}
|
|
// miner-ui-5: this machine's ladder record (state.ladder, src/ladder.rs) per scenario
|
|
function ladderNow(t) {
|
|
const base = { first_block_at: 0, first_block_card: '', first_block_hash: '', first_block_daa: 0, days_mined_30: 0, blocks_30d: 0, blocks_today: 0, days: [], cards_first: {}, blocks: [], votes_signed: 0, last_vote_index: 0, last_vote_at: 0, last_signed_locked: 0, signed_locked_count: 0, streak_s: 0, shards: [], milestone: null, first_synced_at: t0 - 2800, first_mining_at: t0 - 2700 };
|
|
const hash = '9f3a7c1e5d2b4a6f8e0c1d3b5a7f9e2c4d6b8a0f1e3c5d7b9a2f4e6c8d0b1a3c21e';
|
|
if (scenario === 'firstblock') return { ...base, first_block_at: t - 70, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, blocks: [{ hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 70, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 2, last_vote_index: 8496, last_vote_at: t - 20, milestone: { kind: 'first', count: 1, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266454, at: t - 70 } };
|
|
if (scenario === 'ladder') return { ...base, first_block_at: t - 7300, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 259200, days_mined_30: 1, blocks_30d: 61, blocks_today: 61, votes_signed: 240, last_vote_index: 8495, last_vote_at: t - 12, last_signed_locked: 8495, signed_locked_count: 238, streak_s: 7100, shards: [{ block: 160390, shard: 3, wei: '1150000000000000000', at: t - 400 }], milestone: { kind: 'hundred', count: 100, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266400, at: t - 50 } };
|
|
return base;
|
|
}
|
|
// miner-ui-5: GET /api/ladder, in src/chainfacts.rs's shape: the devnet's finality params (api/live, 7 Oct 2026),
|
|
// four keys relayed through the observer, this machine's key 8fafda27 ranked per scenario
|
|
function ladderReply(t) {
|
|
const mine = scenario === 'firstblock' ? { id: '8fafda27', blocks: 1, voter: false, participation: 0, stripped_until_daa: 0, revealed: false, rank: 4 }
|
|
: scenario === 'ladder' ? { id: '8fafda27', blocks: 61, voter: true, participation: 1, stripped_until_daa: 0, revealed: true, rank: 3 }
|
|
: scenario === 'firstwait' ? null : { id: '8fafda27', blocks: 251, voter: true, participation: 1, stripped_until_daa: 0, revealed: true, rank: 2 };
|
|
return { ok: true, source: 'observer', read_at: t, cached_age_s: 0, rpc: { weights: 'getFinalityWeights', checkpoints: 'getFinalityCheckpoints', stats: '/api/stats', relay: 'igneum.network/api/live (the observer\'s copy of the node\'s reply)' },
|
|
params: { dust: 5, weight_window: 7200, presence_window: 20, checkpoint_interval: 30, min_daa: 7200 }, checkpoint_index: 8495, daa_score: 266454, latest_locked_index: 8495, next_index: 8496, finality_active: true,
|
|
total_weight: 6000, active_weight: 5900.5, voters: 14, keys_listed: 4, keys_cap: 64,
|
|
network: { hashrate_hps: 764944722, miner_ign_per_block: 4.88044084, ign_per_block: 6.10055105, ramp_factor: 0.192519, daa: 266454, blocks_per_day_measured: 92280, bps: 1, stale: false },
|
|
mine: mine ? [mine] : [], best: mine };
|
|
}
|
|
function stateNow() {
|
|
const s = clone(recState);
|
|
const t = now();
|
|
s.now = t;
|
|
s.ladder = ladderNow(t);
|
|
s.installed_at = t0 - 3000;
|
|
if (scenario === 'firstwait') { s.mining.accepted_total = 0; s.mining.accepted_session = 0; s.mining.cards[0].accepted = 0; s.mining.found = []; s.finality = { ...s.finality, last_lock: 0, votes: 0 }; }
|
|
if (scenario === 'firstblock') { s.mining.accepted_total = 1; s.mining.accepted_session = 1; s.mining.cards[0].accepted = 1; }
|
|
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 captured state (IGNEUM_MOCK_STATE) is replayed as it was: the machine's own name, cards, rate and numbers
|
|
if (process.env.IGNEUM_MOCK_STATE) { s.node.blocks = recState.node.blocks; s.node.headers = recState.node.headers; s.node.daa = recState.node.daa; return s; }
|
|
// 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 (none before the first block)
|
|
const extra = []; if (scenario !== 'firstwait' && scenario !== 'firstblock') for (let k = t0; k < t; k += 9.3) extra.push(k + 0.4);
|
|
if (scenario === 'firstwait') { s.mining.found = []; s.mining.accepted_total = 0; s.mining.accepted_session = 0; s.mining.cards[0].accepted = 0; s.events = s.events.filter(e => e.kind !== 'block'); }
|
|
if (scenario === 'firstblock') { s.mining.found = [t - 70]; s.events = [{ kind: 'block', t: t - 70, text: 'first block: found by ' + s.mining.cards[0].name + ' (block card)' }, { kind: 'block', t: t - 70, text: 'block accepted by the node (' + s.mining.cards[0].name + ')' }].concat(s.events.filter(e => e.kind !== 'block')).slice(0, 40); }
|
|
if (scenario === 'ladder') { s.settings.prove = true; s.proving = { ...s.proving, enabled: true, available: true, status: 'idle', paid: 1, paid_wei: '1150000000000000000', last_paid: 'block 160390 shard 3' }; s.events = [{ kind: 'proving', t: t - 400, text: 'block 160390 shard 3 paid 1.15 IGN' }, { kind: 'block', t: t - 50, text: 'block 100 of this machine: found by ' + s.mining.cards[0].name + ' (block card)' }].concat(s.events).slice(0, 40); }
|
|
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/live' && req.method === 'GET') {
|
|
if (recLive && recLive.blocks) {
|
|
// replayed with its newest block 3 s behind now, so the module's window shows it as it was captured
|
|
const newest = Math.max(...recLive.blocks.map(b => b.ts || 0)), dt = Date.now() - 3000 - newest;
|
|
const d = JSON.parse(JSON.stringify(recLive)); d.blocks.forEach(b => { b.ts = (b.ts || newest) + dt; if (!b.miner) b.miner = 'unknown'; }); d.now = Math.floor(Date.now() / 1000); return json(res, d);
|
|
}
|
|
return json(res, liveNow(parseInt(u.searchParams.get('window') || '120', 10)));
|
|
}
|
|
if (rest === '/api/ladder' && req.method === 'GET') return json(res, ladderReply(now()));
|
|
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/card') { let bytes = 0; try { bytes = Buffer.from((JSON.parse(body).png || '').split(',')[1] || '', 'base64').length; } catch (e) { } return json(res, { ok: true, path: '/Users/you/Library/Application Support/Igneum/cards/igneum-block-block-1-' + Math.floor(now()) + '.png', bytes }); }
|
|
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));
|