diff --git a/broadcaster/README.md b/broadcaster/README.md index cc5cd16..ef15770 100644 --- a/broadcaster/README.md +++ b/broadcaster/README.md @@ -22,7 +22,7 @@ if Telegram or the service goes down, the pool is unaffected. | **Block lost** | a block it announced turns `orphaned` or `rejected` | | **Pool quiet / back** | no share accepted for `STALE_SHARES_SEC` (default 15m), and when shares resume | | **Health failing / recovered** | `/health` changes state, with the failing checks listed | -| **Pinned live message** (`BROADCASTER_LIVE=1`) | one message, pinned unless `BROADCASTER_LIVE_PIN=0`, edited every `BROADCASTER_LIVE_MS` (default 5m) with current stats and the last block | +| **Pinned live message** (`BROADCASTER_LIVE=1`) | one message, pinned unless `BROADCASTER_LIVE_PIN=0`, edited every `BROADCASTER_LIVE_MS` (default 5m) with current stats, the chain tip and the pool's last mined block | | **`/pool_status`** (`BROADCASTER_COMMANDS=1`) | when someone sends it in the chat: the current stats, as a reply in the same topic, at most once per `BROADCASTER_COMMAND_COOLDOWN_SEC` (default 30) | | **Periodic update** (`BROADCASTER_SUMMARY_HOURS=N`) | the same stats as a new message every N hours, on UTC boundaries (6 → 00, 06, 12, 18 UTC) | diff --git a/broadcaster/lib/messages.js b/broadcaster/lib/messages.js index 84c32a7..c144b3d 100644 --- a/broadcaster/lib/messages.js +++ b/broadcaster/lib/messages.js @@ -101,18 +101,28 @@ export function digest({ status, blocks, poolName, publicUrl, nowSec }) { ); } -/* The newest block that still counts: pending or confirmed. An orphaned or - * rejected one is not the pool's last block. `blocks` is newest first. */ +/* The newest block THIS POOL mined that still counts: pending or confirmed. + * An orphaned or rejected one is not the pool's last block. `blocks` is + * newest first. Labelled "mined" so it is never read as the chain's height, + * which chainTipLine gives. */ export function lastBlockLine(blocks, nowSec) { const b = (blocks ?? []).find((x) => x.status === 'pending' || x.status === 'confirmed'); - if (!b) return 'Last block: none yet'; + if (!b) return 'Last mined block: none yet'; const ago = b.ts != null ? ` · ${fmtAgo(nowSec - b.ts)} ago` : ''; - return `Last block: #${fmtN(b.height)}${ago}`; + return `Last mined block: #${fmtN(b.height)}${ago}`; +} + +/* The network's latest block, as the pool's node sees it, by anyone. Null + * when the dashboard has no node status yet. */ +export function chainTipLine(node, nowSec) { + if (node?.tip_height == null) return null; + const ago = node.tip_observed_at != null ? ` · ${fmtAgo(nowSec - node.tip_observed_at)} ago` : ''; + return `Chain tip: #${fmtN(node.tip_height)}${ago}`; } /* The pinned message, the periodic summary and the /pool_status reply show * the same numbers; only the heading and the time line differ. The last - * block line appears whenever the blocks list was read. */ + * mined block line appears whenever the blocks list was read. */ function snapshot({ status, blocks, poolName, publicUrl, nowSec, flowing, title, timeLabel }) { const p = status.pool; const lastShare = p.last_share_ts ? fmtAgo(nowSec - p.last_share_ts) + ' ago' : 'never'; @@ -121,6 +131,7 @@ function snapshot({ status, blocks, poolName, publicUrl, nowSec, flowing, title, '', ...statLines(p), `Last share: ${lastShare}`, + chainTipLine(status.node, nowSec), blocks !== undefined ? lastBlockLine(blocks, nowSec) : null, `Blocks (all time): ${fmtN(p.blocks_lifetime)}`, '', diff --git a/broadcaster/test/broadcaster.test.js b/broadcaster/test/broadcaster.test.js index 7624ee8..8099936 100644 --- a/broadcaster/test/broadcaster.test.js +++ b/broadcaster/test/broadcaster.test.js @@ -287,9 +287,20 @@ test('live message and summary show the last counted block', async () => { block('lost', 101, 'orphaned', T0 / 1000 - 600), ]; await b.tick(T0); - assert.match(telegram.posts[0], /Last block: #100<\/b> · 2h 0m ago/); + assert.match(telegram.posts[0], /Last mined block: #100<\/b> · 2h 0m ago/); dashboard.s = status({ lastShareTs: (T0 + 60 * MIN) / 1000 }); await b.tick(T0 + 60 * MIN); assert.match(telegram.posts[1], /— update/); - assert.match(telegram.posts[1], /Last block: #100<\/b>/); + assert.match(telegram.posts[1], /Last mined block: #100<\/b>/); +}); + +test('chain tip line appears with node status and is left out without it', async () => { + const { b, dashboard, telegram } = setup({ live: true, digestHour: null }); + dashboard.s.node = { tip_height: 105, tip_observed_at: T0 / 1000 - 90 }; + await b.tick(T0); + assert.match(telegram.posts[0], /Chain tip: #105 · 1m ago/); + + const other = setup({ live: true, digestHour: null }); + await other.b.tick(T0); + assert.doesNotMatch(other.telegram.posts[0], /Chain tip/); }); diff --git a/broadcaster/test/commands.test.js b/broadcaster/test/commands.test.js index dc4376b..be2c6d5 100644 --- a/broadcaster/test/commands.test.js +++ b/broadcaster/test/commands.test.js @@ -14,6 +14,7 @@ function status() { db_ready: true, hashrate: 1e12, hashrate_1h: 2e12, hashrate_5m: 3e12, workers_active: 4, accepted: 1000, rejected: 5, reject_rate_pct: 0.5, best_share_24h: 123456, blocks_lifetime: 7, last_share_ts: T0 / 1000 - 5, }, + node: { tip_height: 970861, tip_observed_at: T0 / 1000 - 1200 }, }; } @@ -69,7 +70,7 @@ test('/pool_status is answered in its topic, as a reply, with the last block', a assert.match(html, /avonpool_beta — status/); assert.match(html, /Hashrate now: 3\.00 TH\/s<\/b> \(5m\)/); assert.match(html, /1,000 accepted · 5 rejected \(0\.50%\)/); - assert.match(html, /Last block: #970,802<\/b> · 1h 0m ago/); + assert.match(html, /Chain tip: #970,861 · 20m ago\nLast mined block: #970,802<\/b> · 1h 0m ago/); assert.match(html, /^🟢/); });