Merge site-ui-4: the EMBER 02 renderer on the home fold and /live (2.0.1 real-data options, the proof inspector, the observatory page)
renderer pack EMBER 02 supplied by Josh, 7 Oct 2026 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
BIN
docs/plans/site-ui-4-shots/dag/demo-feed-proven-1000-dark.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/plans/site-ui-4-shots/dag/home-1440-dark-mid.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/plans/site-ui-4-shots/dag/home-1440-light-mid.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
docs/plans/site-ui-4-shots/dag/home-390-dark-mid.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
docs/plans/site-ui-4-shots/dag/home-390-light-mid.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
595
docs/plans/site-ui-4-shots/dag/igneum-chain-scene.html
Normal file
BIN
docs/plans/site-ui-4-shots/dag/live-1440-dark-mid.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
docs/plans/site-ui-4-shots/dag/live-1440-dark-selected.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
docs/plans/site-ui-4-shots/dag/live-1440-light-mid.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
docs/plans/site-ui-4-shots/dag/live-1440-light-selected.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
docs/plans/site-ui-4-shots/dag/live-390-dark-mid.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/plans/site-ui-4-shots/dag/live-390-dark-selected.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/plans/site-ui-4-shots/dag/live-390-light-mid.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/plans/site-ui-4-shots/dag/live-390-light-selected.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/plans/site-ui-4-shots/dag/live-scene-10s.gif
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
docs/plans/site-ui-4-shots/dag/live-scene-10s.mp4
Normal file
BIN
docs/plans/site-ui-4-shots/dag/live-scene-10s.webm
Normal file
BIN
docs/plans/site-ui-4-shots/dag/pack-preview.mp4
Normal file
BIN
docs/plans/site-ui-4-shots/dag/side-by-side-live-vs-demo.jpg
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
docs/plans/site-ui-4-shots/live-1440-dark-fold.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
docs/plans/site-ui-4-shots/live-1440-dark-full.jpg
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
docs/plans/site-ui-4-shots/live-1440-dark-inspector.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
docs/plans/site-ui-4-shots/live-1440-light-fold.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/plans/site-ui-4-shots/live-1440-light-full.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
docs/plans/site-ui-4-shots/live-390-dark-fold.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/plans/site-ui-4-shots/live-390-dark-full.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
docs/plans/site-ui-4-shots/live-390-light-fold.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/plans/site-ui-4-shots/live-390-light-full.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
330
docs/plans/site-ui-4-shots/pack/results.json
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
{
|
||||
"passed": 73,
|
||||
"total": 73,
|
||||
"results": [
|
||||
{
|
||||
"test": "Standalone preview renders without JS errors",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Standalone preview makes no network requests",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Both renderers use the same block records",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Sample contains six miner keys",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Visible source label says simulated",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Original mount API is exposed",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Zoom-in control works",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Zoom-out control works",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Ordinary page scrolling is not captured",
|
||||
"passed": true,
|
||||
"evidence": {
|
||||
"prevented": false,
|
||||
"w": 90
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": "Modified wheel zooms the graph",
|
||||
"passed": true,
|
||||
"evidence": {
|
||||
"prevented": true,
|
||||
"w": 103
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": "Keyboard inspection updates the details",
|
||||
"passed": true,
|
||||
"evidence": "e231e4cf43678ba5"
|
||||
},
|
||||
{
|
||||
"test": "Escape clears selection",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Your-block filter is active",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Selected-chain filter is active",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Pause button updates state and accessibility",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Pause freezes displayed records while receiving data",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Pause freezes the rendered pixels",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Paused graph has no continuous RAF loop",
|
||||
"passed": true,
|
||||
"evidence": {
|
||||
"before": 16,
|
||||
"after": 16
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": "Resume applies buffered block records",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Sample block control selects a new own block",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Sample proof control advances shard states",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Sample checkpoint control produces an explicit locked record",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Accessible table contains block records",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Table selection opens the matching block",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Light theme control works",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Manual reduced motion propagates to both visuals",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "System reduced-motion changes stop continuous drawing",
|
||||
"passed": true,
|
||||
"evidence": {
|
||||
"before": 70,
|
||||
"after": 70
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": "Motion resumes when the system preference changes",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Offscreen main graph suspends animation",
|
||||
"passed": true,
|
||||
"evidence": {
|
||||
"before": 78,
|
||||
"after": 78
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": "Main preview remains free of JS errors",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Responsive layout 320px has no horizontal overflow",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Compact renderer draws when scrolled into view at 320px",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Responsive preview 320px has no JS errors",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Responsive layout 390px has no horizontal overflow",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Compact renderer draws when scrolled into view at 390px",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Responsive preview 390px has no JS errors",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Responsive layout 768px has no horizontal overflow",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Compact renderer draws when scrolled into view at 768px",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Responsive preview 768px has no JS errors",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Responsive layout 1024px has no horizontal overflow",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Compact renderer draws when scrolled into view at 1024px",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Responsive preview 1024px has no JS errors",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Responsive layout 1440px has no horizontal overflow",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Compact renderer draws when scrolled into view at 1440px",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Responsive preview 1440px has no JS errors",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Valid observer payload is accepted",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Renderer does not mutate supplied records",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Age and selected-chain status do not imply proof or finality",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Existing records update miner, parents and header fields",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Malformed envelope is rejected without throwing",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Invalid block records are rejected without throwing",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Prototype-like hashes are safe map keys",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Tooltip treats incoming text as text, not markup",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Unknown shard state is not promoted to planned or verified",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Zoom has a 30-second lower bound",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Zoom has a 300-second upper bound",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "ResizeObserver tracks container-only resizes",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Checkpoint is not snapped to a different blue score",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Exact checkpoint score renders a pending marker",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Weight alone does not infer checkpoint lock",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Stale feed preserves records and reports stale",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Stale feed does not animate as fresh activity",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Mounting again destroys the previous instance",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Destroy stops animation and rejects future pushes",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Polling preserves existing endpoint query parameters",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Polling requests fresh data with an abort signal",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Overlapping polls are prevented",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Queued zoom request uses latest window",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Observer response reaches rendered model",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Destroy aborts an in-flight observer request",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "New module runs inside the supplied original HTML host",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
},
|
||||
{
|
||||
"test": "Original host pause control still works",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"test": "Original host theme control remains compatible",
|
||||
"passed": true,
|
||||
"evidence": []
|
||||
}
|
||||
],
|
||||
"scope": "Chromium headless; embedded standalone HTML; mocked read-only observer; original uploaded HTML host. Not a live devnet or native mining-engine test."
|
||||
}
|
||||
74
docs/plans/site-ui-4-shots/pack/verify-local.log
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
PASS Standalone preview renders without JS errors
|
||||
PASS Standalone preview makes no network requests
|
||||
PASS Both renderers use the same block records
|
||||
PASS Sample contains six miner keys
|
||||
PASS Visible source label says simulated
|
||||
PASS Original mount API is exposed
|
||||
PASS Zoom-in control works
|
||||
PASS Zoom-out control works
|
||||
PASS Ordinary page scrolling is not captured
|
||||
PASS Modified wheel zooms the graph
|
||||
PASS Keyboard inspection updates the details
|
||||
PASS Escape clears selection
|
||||
PASS Your-block filter is active
|
||||
PASS Selected-chain filter is active
|
||||
PASS Pause button updates state and accessibility
|
||||
PASS Pause freezes displayed records while receiving data
|
||||
PASS Pause freezes the rendered pixels
|
||||
PASS Paused graph has no continuous RAF loop
|
||||
PASS Resume applies buffered block records
|
||||
PASS Sample block control selects a new own block
|
||||
PASS Sample proof control advances shard states
|
||||
PASS Sample checkpoint control produces an explicit locked record
|
||||
PASS Accessible table contains block records
|
||||
PASS Table selection opens the matching block
|
||||
PASS Light theme control works
|
||||
PASS Manual reduced motion propagates to both visuals
|
||||
PASS System reduced-motion changes stop continuous drawing
|
||||
PASS Motion resumes when the system preference changes
|
||||
PASS Offscreen main graph suspends animation
|
||||
PASS Main preview remains free of JS errors
|
||||
PASS Responsive layout 320px has no horizontal overflow
|
||||
PASS Compact renderer draws when scrolled into view at 320px
|
||||
PASS Responsive preview 320px has no JS errors
|
||||
PASS Responsive layout 390px has no horizontal overflow
|
||||
PASS Compact renderer draws when scrolled into view at 390px
|
||||
PASS Responsive preview 390px has no JS errors
|
||||
PASS Responsive layout 768px has no horizontal overflow
|
||||
PASS Compact renderer draws when scrolled into view at 768px
|
||||
PASS Responsive preview 768px has no JS errors
|
||||
PASS Responsive layout 1024px has no horizontal overflow
|
||||
PASS Compact renderer draws when scrolled into view at 1024px
|
||||
PASS Responsive preview 1024px has no JS errors
|
||||
PASS Responsive layout 1440px has no horizontal overflow
|
||||
PASS Compact renderer draws when scrolled into view at 1440px
|
||||
PASS Responsive preview 1440px has no JS errors
|
||||
PASS Valid observer payload is accepted
|
||||
PASS Renderer does not mutate supplied records
|
||||
PASS Age and selected-chain status do not imply proof or finality
|
||||
PASS Existing records update miner, parents and header fields
|
||||
PASS Malformed envelope is rejected without throwing
|
||||
PASS Invalid block records are rejected without throwing
|
||||
PASS Prototype-like hashes are safe map keys
|
||||
PASS Tooltip treats incoming text as text, not markup
|
||||
PASS Unknown shard state is not promoted to planned or verified
|
||||
PASS Zoom has a 30-second lower bound
|
||||
PASS Zoom has a 300-second upper bound
|
||||
PASS ResizeObserver tracks container-only resizes
|
||||
PASS Checkpoint is not snapped to a different blue score
|
||||
PASS Exact checkpoint score renders a pending marker
|
||||
PASS Weight alone does not infer checkpoint lock
|
||||
PASS Stale feed preserves records and reports stale
|
||||
PASS Stale feed does not animate as fresh activity
|
||||
PASS Mounting again destroys the previous instance
|
||||
PASS Destroy stops animation and rejects future pushes
|
||||
PASS Polling preserves existing endpoint query parameters
|
||||
PASS Polling requests fresh data with an abort signal
|
||||
PASS Overlapping polls are prevented
|
||||
PASS Queued zoom request uses latest window
|
||||
PASS Observer response reaches rendered model
|
||||
PASS Destroy aborts an in-flight observer request
|
||||
PASS New module runs inside the supplied original HTML host
|
||||
PASS Original host pause control still works
|
||||
PASS Original host theme control remains compatible
|
||||
RESULT 73 / 73
|
||||
|
|
@ -330,8 +330,8 @@
|
|||
</div>
|
||||
<div class="grid c2">
|
||||
<div class="dl-list" style="margin:0">
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="primary">Windows <span data-dl-meta="miner-windows">v0.3.17 · 62.6 MB</span></a>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg">macOS <span data-dl-meta="miner-mac">v0.3.17 · 41.9 MB</span></a>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="primary">Windows <span data-dl-meta="miner-windows">v0.3.18 · 62.6 MB</span></a>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg">macOS <span data-dl-meta="miner-mac">v0.3.18 · 42.0 MB</span></a>
|
||||
<a href="/miner#hive">Linux · HiveOS <span>the flight sheet, on the miner page</span></a>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -354,7 +354,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<a class="dl-bar" href="/miner#get"><b>Download the miner</b><span data-dl-meta="miner-windows">v0.3.17 · 62.6 MB</span></a>
|
||||
<a class="dl-bar" href="/miner#get"><b>Download the miner</b><span data-dl-meta="miner-windows">v0.3.18 · 62.6 MB</span></a>
|
||||
|
||||
<!-- footer:start -->
|
||||
<footer class="foot">
|
||||
|
|
|
|||
549
site/live-dag.js
|
|
@ -1,236 +1,349 @@
|
|||
/* Igneum live DAG view (docs/plans/site-ui-3.md section 6, 6 October 2026).
|
||||
One module for /live and the home page. Reads /api/live only; every pixel is a block, edge or checkpoint the observer
|
||||
stored. Blocks are placed by header time on x (the axis is labelled in DAA score) and by miner lane on y. Parent edges
|
||||
are drawn; the edge between two selected-chain blocks is heavier and the chain glows as one path. Colour words are the
|
||||
ledger's: pending, included (blue), excluded (red), proven (every shard verified or paid), locked (a checkpoint block).
|
||||
Checkpoints are vertical bands at their blue score with the lock weight; a locked band is solid, a pending one dashed.
|
||||
The view scrolls at chain speed, new blocks arrive from the right with a brief glow. The page's scroll passes through the
|
||||
scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene (engaged, released by Escape or a click
|
||||
outside; opts.chip is the "scroll to zoom, Esc to release" element); window 30 to 300 s (the API's ?window=); a pause button;
|
||||
hover and tap show a block's details.
|
||||
|
||||
Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, onEngage, pauseButton, tooltip, chip })
|
||||
- url: the /api/live endpoint (default '/api/live'); poll: false stops the module's own 2 s fetch, the host then calls
|
||||
dag.push(reply) with a reply of the same shape ({ok, state:{stale, age_s, height, ...}, blocks:[...], finality:{checkpoints:[...]}, proving})
|
||||
whenever it has new data (every poll or every new block, either is fine; blocks already on screen are updated in place).
|
||||
- block record (the /api/live shape): hash (string, 16 hex), ts (ms, header time), blue_score, daa (numbers), parents ([hash]),
|
||||
chain (bool, selected chain), color ('blue' | 'red' | 'pending'), miner (string lane key, 8 hex of the vote key hash),
|
||||
locked (bool, a locked checkpoint block), final (bool, at or before the newest lock), shards ([{state: 'planned' | 'proving' |
|
||||
'verified' | 'paid'}]), proven (bool, every shard verified or paid). Checkpoints: {index, blue_score, state: 'locked' | 'pending', fraction_total}.
|
||||
- mine: a miner lane key (string) or function(block) -> bool; the user's own blocks take a molten ring and the lane is labelled "you".
|
||||
- The legend is the host's; the module draws no HTML. Colours come from the CSS custom properties on :root (--ember, --ember-hi,
|
||||
--molten, --bone, --ash, --line, --line-2, --row or --obsidian), re-read on data-theme and prefers-color-scheme changes.
|
||||
- Sizing: the canvas fills its CSS box (width and height from the stylesheet); 280 px wide works, 600 or more shows lane labels well.
|
||||
onState(state, reason): 'connecting' | 'live' | 'stale' | 'failed'; onData(d) with every fresh reply. */
|
||||
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026) with the real-data presentation options of 2.0.1:
|
||||
* maxLanes (own key + top keys + others, default 7), minNode (node half-size floor, default 9.5), hairlineAlpha (non-chain
|
||||
* edges, default .12, drawn only within laneReach lanes, default 2, and only a merged block's first parent unless selected),
|
||||
* othersScale (the others lane's size and alpha, default .75), provenGlow (a glow and ring on proven blocks, default on).
|
||||
* Replacement for the supplied live-dag.js. No framework, network writes or synthetic records.
|
||||
* Existing mount/push options and public methods are retained; see README.md for additions.
|
||||
* Every node, parent edge, proof segment and checkpoint is derived from the observer reply.
|
||||
* Motion interpolates presentation ONLY. It never promotes proof, inclusion or finality.
|
||||
*/
|
||||
(function (root) {
|
||||
'use strict';
|
||||
var COL = {
|
||||
ember: '#F2541B', emberHi: '#FF6A2B', molten: '#FFB35C', bone: '#F4F1EC', ash: '#9A9A9E', line: '#2A2A30', line2: '#3A3A42', obsidian: '#0C0C0E',
|
||||
red: '#B0362B'
|
||||
};
|
||||
function cssVar(name, fallback) { try { var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || fallback; } catch (e) { return fallback; } }
|
||||
function readTheme() {
|
||||
COL.ember = cssVar('--ember', COL.ember); COL.emberHi = cssVar('--ember-hi', COL.emberHi); COL.molten = cssVar('--molten', COL.molten);
|
||||
COL.bone = cssVar('--bone', COL.bone); COL.ash = cssVar('--ash', COL.ash); COL.line = cssVar('--line', COL.line); COL.line2 = cssVar('--line-2', COL.line2);
|
||||
COL.obsidian = cssVar('--row', cssVar('--obsidian', COL.obsidian));
|
||||
var instances = new WeakMap();
|
||||
var TAU = Math.PI * 2;
|
||||
function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }
|
||||
function finite(v) { return typeof v === 'number' && Number.isFinite(v); }
|
||||
function num(v) { return finite(v) ? v : null; }
|
||||
function fmt(v) { return v == null ? 'unknown' : Number(v).toLocaleString('en-GB'); }
|
||||
function rgba(c, a) {
|
||||
var s = String(c).trim(), h = s.replace('#', ''), rgb;
|
||||
if (/^#[0-9a-f]{3}$/i.test(s)) h = h.split('').map(function (x) { return x + x; }).join('');
|
||||
if (/^[0-9a-f]{6}$/i.test(h)) return 'rgba(' + parseInt(h.slice(0,2),16) + ',' + parseInt(h.slice(2,4),16) + ',' + parseInt(h.slice(4,6),16) + ',' + a + ')';
|
||||
rgb = s.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i);
|
||||
return rgb ? 'rgba(' + rgb[1] + ',' + rgb[2] + ',' + rgb[3] + ',' + a + ')' : s;
|
||||
}
|
||||
function rounded(ctx, x, y, w, h, r) {
|
||||
r = Math.max(0, Math.min(r, w/2, h/2)); ctx.beginPath(); ctx.moveTo(x+r,y);
|
||||
ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
|
||||
ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
|
||||
}
|
||||
function cloneBlock(b) {
|
||||
return {hash:b.hash,ts:b.ts,blue_score:b.blue_score,blue:b.blue_score,daa:b.daa,
|
||||
parents:b.parents.slice(),chain:b.chain,color:b.color,miner:b.miner,locked:b.locked,
|
||||
final:b.final,proven:b.proven,shards:b.shards.map(function(s){return {state:s.state};})};
|
||||
}
|
||||
function normalize(b) {
|
||||
if (!b || typeof b.hash !== 'string' || !b.hash.length || b.hash.length>256 || !finite(b.ts) || b.ts<0) return null;
|
||||
return {hash:b.hash,ts:b.ts,blue_score:num(b.blue_score),daa:num(b.daa),
|
||||
parents:Array.isArray(b.parents)?Array.from(new Set(b.parents.filter(function(p){return typeof p==='string' && p!==b.hash;}))).slice(0,128):[],
|
||||
chain:b.chain===true,color:['blue','red','pending'].includes(b.color)?b.color:'pending',
|
||||
miner:typeof b.miner==='string'?b.miner.slice(0,256):'unknown',locked:b.locked===true,final:b.final===true,proven:b.proven===true,
|
||||
shards:Array.isArray(b.shards)?b.shards.slice(0,256).map(function(s){return {state:s && ['planned','proving','verified','paid'].includes(s.state)?s.state:'unknown'};}):[]};
|
||||
}
|
||||
function rgba(hex, a) { var h = hex.replace('#', ''); if (h.length !== 6) return hex; return 'rgba(' + parseInt(h.slice(0, 2), 16) + ',' + parseInt(h.slice(2, 4), 16) + ',' + parseInt(h.slice(4, 6), 16) + ',' + a + ')'; }
|
||||
function fmt(n) { return n === null || n === undefined ? 'pending' : Number(n).toLocaleString('en-GB'); }
|
||||
function fmtAge(sec) { sec = Math.round(sec); return sec < 90 ? sec + ' s' : sec < 5400 ? Math.round(sec / 60) + ' min' : Math.round(sec / 3600) + ' h'; }
|
||||
|
||||
function mount(canvas, opts) {
|
||||
opts = opts || {};
|
||||
var compact = !!opts.compact, windowS = Math.max(30, Math.min(300, opts.window || (compact ? 90 : 120)));
|
||||
var reduce = root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var isMine = typeof opts.mine === 'function' ? opts.mine : opts.mine ? function (b) { return b.miner === opts.mine; } : null;
|
||||
var ctx = canvas.getContext('2d'), dpr = Math.min(root.devicePixelRatio || 1, 2), W = 0, H = 0;
|
||||
var blocks = [], byHash = {}, lanes = [], laneOf = {}, cps = [], data = null, state = 'connecting', reason = 'connecting';
|
||||
var paused = false, offsetMs = 0, hover = null, pinned = null, lastPoll = 0, failures = 0, running = false, inView = true, lastT = 0;
|
||||
var vnow = function () { return Date.now() - 2500 - offsetMs; }; // the right edge sits 2.5 s behind the clock so a block has arrived before it is drawn
|
||||
var padL = compact ? 10 : 56, padR = 14, padT = compact ? 12 : 22, padB = compact ? 10 : 26;
|
||||
|
||||
function size() { W = canvas.clientWidth; H = canvas.clientHeight; canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); layout(); }
|
||||
root.addEventListener('resize', size);
|
||||
|
||||
// ---- data ----
|
||||
function apply(d) {
|
||||
data = d;
|
||||
var seen = {}, now = performance.now();
|
||||
d.blocks.forEach(function (b) {
|
||||
seen[b.hash] = 1;
|
||||
var e = byHash[b.hash];
|
||||
if (e) { e.chain = !!b.chain; e.color = b.color || 'pending'; e.locked = !!b.locked; e.final = !!b.final; e.shards = b.shards || []; e.proven = !!b.proven; e.blue = b.blue_score; e.daa = b.daa; return; }
|
||||
e = { hash: b.hash, ts: b.ts, blue: b.blue_score, daa: b.daa, parents: b.parents || [], chain: !!b.chain, color: b.color || 'pending', miner: b.miner || '?', locked: !!b.locked, final: !!b.final, shards: b.shards || [], proven: !!b.proven, born: now, x: 0, y: 0 };
|
||||
byHash[b.hash] = e; blocks.push(e);
|
||||
opts=opts||{};
|
||||
if (!canvas || typeof canvas.getContext!=='function') throw new TypeError('IgneumDag.mount requires a canvas element.');
|
||||
if (instances.has(canvas)) instances.get(canvas).destroy();
|
||||
var ctx=canvas.getContext('2d'); if(!ctx) throw new Error('A 2D canvas context is required.');
|
||||
var doc=canvas.ownerDocument, compact=!!opts.compact;
|
||||
var windowS=clamp(finite(opts.window)?opts.window:(compact?90:120),30,300);
|
||||
var maxBlocks=clamp(finite(opts.maxBlocks)?opts.maxBlocks:6000,100,20000);
|
||||
var lag=finite(opts.lagMs)?clamp(opts.lagMs,0,10000):2500;
|
||||
var fps=clamp(finite(opts.fps)?opts.fps:30,10,60), minFrame=1000/fps;
|
||||
var maxLanes=clamp(finite(opts.maxLanes)?opts.maxLanes:7,2,12), minNode=finite(opts.minNode)?opts.minNode:9.5, hairAlpha=finite(opts.hairlineAlpha)?opts.hairlineAlpha:.12, laneReach=finite(opts.laneReach)?opts.laneReach:2, othersScale=finite(opts.othersScale)?opts.othersScale:.75, provenGlow=opts.provenGlow!==false;
|
||||
var mq=root.matchMedia?root.matchMedia('(prefers-reduced-motion: reduce)'):null;
|
||||
var reduced=mq?mq.matches:false, manualMotion=true;
|
||||
var mediaTheme=root.matchMedia?root.matchMedia('(prefers-color-scheme: dark)'):null;
|
||||
var col={}, W=0,H=0,dpr=1,padL=100,padR=22,padT=57,padB=37;
|
||||
var model=new Map(), view=new Map(), blocks=[], cps=[], latestCps=[], lanes=[], laneOf=new Map();
|
||||
var state='connecting',reason='Waiting for observer',destroyed=false,paused=false,following=true,engaged=false;
|
||||
var fixedRight=0,pausedRight=0,staleRight=null,reducedRight=Date.now()-lag, lastGood=0, lastData=null;
|
||||
var selected=null,hover=null,filter='all',queuedCount=0,invalid=0,omitted=0;
|
||||
var visible=true,raf=0,lastPaint=-Infinity,frameCount=0,lastT=0,freezeT=0;
|
||||
var pollTimer=0,healthTimer=0,timeout=0,controller=null,inflight=false,pollAgain=false,failures=0;
|
||||
var remove=[],ro=null,io=null,mo=null,drag=null,pinch=null;
|
||||
var lastCheckpointStates=new Map(),checkpointBursts=new Map();
|
||||
var initialAttributes={tabindex:canvas.getAttribute('tabindex'),role:canvas.getAttribute('role'),label:canvas.getAttribute('aria-label'),touch:canvas.style.touchAction,cursor:canvas.style.cursor};
|
||||
var tip=opts.tooltip||null,chip=opts.chip||null;
|
||||
var mine=typeof opts.mine==='function'?opts.mine:opts.mine?function(b){return b.miner===opts.mine;}:function(){return false;};
|
||||
function own(b){try{return !!mine(b);}catch(e){return false;}}
|
||||
function emit(name){if(destroyed || typeof opts[name]!=='function')return;var args=[].slice.call(arguments,1);try{opts[name].apply(null,args);}catch(e){console.error('IgneumDag '+name+' callback:',e);}}
|
||||
function on(target,name,handler,config){target.addEventListener(name,handler,config);remove.push(function(){target.removeEventListener(name,handler,config);});}
|
||||
function theme(){
|
||||
var s=getComputedStyle(doc.documentElement);
|
||||
function c(n,f){return s.getPropertyValue(n).trim()||f;}
|
||||
col={ember:c('--ember','#F2541B'),emberHi:c('--ember-hi','#FF6A2B'),molten:c('--molten','#FFB35C'),
|
||||
bone:c('--bone','#F4F1EC'),ash:c('--ash','#9A9A9E'),line:c('--line','#2A2A30'),line2:c('--line-2','#3A3A42'),
|
||||
bg:c('--row',c('--obsidian','#111114')),blue:c('--included','#3B7DD8'),red:c('--excluded','#B0362B'),
|
||||
surface:c('--graphite','#16161A')};
|
||||
redraw();
|
||||
}
|
||||
function rightTime(){
|
||||
if(paused)return pausedRight;
|
||||
if(!following)return fixedRight;
|
||||
if(state!=='live' && staleRight!==null)return staleRight;
|
||||
return reduced||!manualMotion?reducedRight:Date.now()-lag;
|
||||
}
|
||||
function xOf(ts,right){return padL+(W-padL-padR)*(1-(right-ts)/(windowS*1000));}
|
||||
function eventXY(ev){var r=canvas.getBoundingClientRect();return {x:(ev.clientX-r.left)*W/(r.width||1),y:(ev.clientY-r.top)*H/(r.height||1)};}
|
||||
function setState(s,r){
|
||||
if(state===s && reason===r)return;
|
||||
if(s!=='live' && state==='live')staleRight=rightTime();
|
||||
state=s;reason=r||null;
|
||||
if(s==='live')staleRight=null;
|
||||
emit('onState',s,reason);redraw();
|
||||
}
|
||||
function layout(){
|
||||
padL=compact||W<510?14:W<750?84:106;padR=compact?12:24;padT=compact?29:58;padB=compact?22:38;
|
||||
var start=rightTime()-windowS*1000, end=rightTime()+lag+1000,counts=new Map(),mineKeys=new Set();
|
||||
blocks.forEach(function(b){if(b.ts>=start && b.ts<=end){counts.set(b.miner,(counts.get(b.miner)||0)+1);if(own(b))mineKeys.add(b.miner);}});
|
||||
var capacity=Math.min(maxLanes,Math.max(2,Math.floor((H-padT-padB)/(compact?26:46)))),ids=Array.from(counts.keys());
|
||||
ids.sort(function(a,b){return (mineKeys.has(b)?1:0)-(mineKeys.has(a)?1:0)||(counts.get(b)-counts.get(a))||a.localeCompare(b);});
|
||||
var chosen=ids.slice(0,ids.length>capacity?capacity-1:capacity);
|
||||
// Preserve existing key order instead of re-ranking every poll. Always keep the user's lane visible.
|
||||
var keep=lanes.filter(function(id){return chosen.includes(id);});
|
||||
chosen.forEach(function(id){if(!keep.includes(id))keep.push(id);});
|
||||
keep.sort(function(a,b){return (mineKeys.has(b)?1:0)-(mineKeys.has(a)?1:0);});
|
||||
lanes=keep;if(ids.length>chosen.length)lanes.push('__others__');
|
||||
laneOf=new Map();lanes.forEach(function(id,i){laneOf.set(id,i);});
|
||||
var now=performance.now();
|
||||
blocks.forEach(function(b){var i=laneOf.has(b.miner)?laneOf.get(b.miner):Math.max(0,lanes.length-1);
|
||||
var y=padT+(i+.5)*(H-padT-padB)/Math.max(1,lanes.length);
|
||||
if(!finite(b.y)){b.y=y;b.fromY=y;b.targetY=y;}
|
||||
if(b.targetY!==y){b.fromY=b.y;b.targetY=y;b.laneAt=now;}
|
||||
});
|
||||
var cut = vnow() - windowS * 1000 - 20000;
|
||||
blocks = blocks.filter(function (b) { if (b.ts < cut && !seen[b.hash]) { delete byHash[b.hash]; return false; } return true; });
|
||||
blocks.sort(function (a, b) { return a.ts - b.ts; });
|
||||
cps = (d.finality && d.finality.checkpoints) || [];
|
||||
}
|
||||
function size(){
|
||||
if(destroyed)return;
|
||||
var nextW=canvas.clientWidth,nextH=canvas.clientHeight,nextDpr=Math.min(root.devicePixelRatio||1,2);
|
||||
if(W===nextW&&H===nextH&&dpr===nextDpr&&canvas.width===Math.round(nextW*nextDpr)){redraw();return;}
|
||||
W=nextW;H=nextH;dpr=nextDpr;
|
||||
canvas.width=Math.round(W*dpr);canvas.height=Math.round(H*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);layout();redraw();
|
||||
}
|
||||
function syncView(){
|
||||
var now=performance.now(),first=view.size===0, next=new Map();
|
||||
model.forEach(function(b,hash){var old=view.get(hash),v=Object.assign({},b);
|
||||
v.born=old?old.born:first?now-3000:now;v.newArrival=old?old.newArrival:!first;v.visibleAt=old?old.visibleAt:null;v.changed=old && (old.proven!==b.proven||old.locked!==b.locked||old.color!==b.color||old.shards.map(function(s){return s.state;}).join(',')!==b.shards.map(function(s){return s.state;}).join(','))?now:old?old.changed:now-3000;
|
||||
v.y=old?old.y:NaN;v.fromY=old?old.fromY:NaN;v.targetY=old?old.targetY:NaN;v.laneAt=old?old.laneAt:now;
|
||||
next.set(hash,v);
|
||||
});
|
||||
view=next;blocks=Array.from(view.values()).sort(function(a,b){return a.ts-b.ts||a.hash.localeCompare(b.hash);});
|
||||
cps=latestCps.map(function(c){return Object.assign({},c);});
|
||||
cps.forEach(function(c){var prev=lastCheckpointStates.get(c.index);if(prev==='pending'&&c.state==='locked')checkpointBursts.set(c.index,now);lastCheckpointStates.set(c.index,c.state);});
|
||||
var cpKeys=new Set(cps.map(function(c){return c.index;}));lastCheckpointStates.forEach(function(_,key){if(!cpKeys.has(key)){lastCheckpointStates.delete(key);checkpointBursts.delete(key);}});
|
||||
layout();
|
||||
if(selected && !view.has(selected)){selected=null;emit('onSelect',null,'expired');}
|
||||
else if(selected)emit('onSelect',cloneBlock(view.get(selected)),'update');
|
||||
if(hover&&!view.has(hover))hover=null;
|
||||
queuedCount=0;
|
||||
}
|
||||
function layout() {
|
||||
// lanes: miners of the visible window ordered by blocks, as many as fit at 22 px, the rest in one "others" lane
|
||||
var vis = vnow() - windowS * 1000, count = {};
|
||||
blocks.forEach(function (b) { if (b.ts >= vis) count[b.miner] = (count[b.miner] || 0) + 1; });
|
||||
var ids = Object.keys(count).sort(function (a, b) { return count[b] - count[a] || (a < b ? -1 : 1); });
|
||||
var usable = H - padT - padB, laneH = compact ? 26 : 22, maxLanes = Math.max(2, Math.floor(usable / laneH));
|
||||
var shown = ids.length > maxLanes ? ids.slice(0, maxLanes - 1) : ids;
|
||||
lanes = shown.slice(); if (ids.length > shown.length) lanes.push('others');
|
||||
laneOf = {}; shown.forEach(function (id, i) { laneOf[id] = i; });
|
||||
}
|
||||
function laneY(b) { var i = b.miner in laneOf ? laneOf[b.miner] : lanes.length - 1; var n = Math.max(1, lanes.length); return padT + (i + 0.5) * ((H - padT - padB) / n); }
|
||||
function xOf(ts) { return padL + (W - padL - padR) * (1 - (vnow() - ts) / (windowS * 1000)); }
|
||||
|
||||
// the host can feed replies itself (poll: false); the same path the fetch takes
|
||||
function push(d) {
|
||||
if (!d || !d.ok || !d.state || !d.blocks) { setState('failed', 'live feed unavailable'); return; }
|
||||
failures = 0;
|
||||
if (d.state.stale) setState('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown'));
|
||||
else setState('live', null);
|
||||
apply(d); if (opts.onData) opts.onData(d); kick();
|
||||
}
|
||||
function poll() {
|
||||
if (opts.poll === false || document.hidden) return;
|
||||
var u = (opts.url || '/api/live') + '?window=' + windowS;
|
||||
fetch(u, { cache: 'no-store' }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function (d) {
|
||||
if (!d || !d.ok || !d.state || !d.blocks) throw new Error('bad reply');
|
||||
failures = 0;
|
||||
if (d.state.stale) { setState('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown')); }
|
||||
else setState('live', null);
|
||||
apply(d); if (opts.onData) opts.onData(d);
|
||||
kick();
|
||||
}).catch(function () { failures++; if (failures >= 2) setState('failed', 'live feed unavailable'); });
|
||||
}
|
||||
function setState(s, r) { if (s === state && r === reason) return; state = s; reason = r; if (opts.onState) opts.onState(s, r); }
|
||||
|
||||
// ---- drawing ----
|
||||
function draw(t) {
|
||||
if (!W || !H) return;
|
||||
var dt = lastT ? Math.min(50, t - lastT) : 16; lastT = t;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
var vis = vnow() - windowS * 1000, n = Math.max(1, lanes.length), lh = (H - padT - padB) / n;
|
||||
// lane rails and labels
|
||||
ctx.lineWidth = 1;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var y = padT + (i + 0.5) * lh;
|
||||
ctx.strokeStyle = rgba(COL.line, 0.9); ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - padR, y); ctx.stroke();
|
||||
if (!compact) { var mineLane = isMine && typeof opts.mine === 'string' && lanes[i] === opts.mine; ctx.fillStyle = mineLane ? COL.molten : COL.ash; ctx.font = (mineLane ? '500' : '400') + ' 10px IBM Plex Mono, monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(mineLane ? 'you' : lanes[i] === 'others' ? 'others' : String(lanes[i]).slice(0, 8), 6, y); }
|
||||
function push(d){
|
||||
if(destroyed)return false;
|
||||
if(!d||d.ok!==true||!d.state||typeof d.state!=='object'||!Array.isArray(d.blocks)){
|
||||
setState('failed','Invalid observer reply; expected {ok, state, blocks}.');return false;
|
||||
}
|
||||
// checkpoint bands at their blue score (x from the chain block carrying that score, else the nearest)
|
||||
cps.forEach(function (c) {
|
||||
var ref = null, best = Infinity;
|
||||
for (var k = blocks.length - 1; k >= 0; k--) { var b = blocks[k]; if (b.blue === null || b.blue === undefined) continue; var dd = Math.abs(b.blue - c.blue_score); if (dd < best) { best = dd; ref = b; } }
|
||||
if (!ref || best > 3 || ref.ts < vis) return;
|
||||
var x = xOf(ref.ts), locked = c.state === 'locked';
|
||||
ctx.strokeStyle = rgba(COL.ember, locked ? 0.55 : 0.3); ctx.lineWidth = locked ? 1.5 : 1; ctx.setLineDash(locked ? [] : [4, 6]);
|
||||
ctx.beginPath(); ctx.moveTo(x, padT - 6); ctx.lineTo(x, H - padB + 6); ctx.stroke(); ctx.setLineDash([]);
|
||||
if (!compact) { ctx.fillStyle = rgba(COL.ember, 0.9); ctx.font = '500 10px IBM Plex Mono, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText((locked ? 'locked ' : 'pending ') + (c.fraction_total !== null && c.fraction_total !== undefined ? Math.round(c.fraction_total * 100) + '%' : ''), x, H - 8); }
|
||||
var now=Date.now(), incoming=[], bad=0;
|
||||
d.blocks.forEach(function(b){var n=normalize(b);if(n)incoming.push(n);else bad++;});
|
||||
if(d.blocks.length && !incoming.length){setState('failed','Observer reply contains no valid block records.');return false;}
|
||||
invalid=bad;lastGood=now;reducedRight=now-lag;failures=0;lastData=d;
|
||||
incoming.forEach(function(b){if(!model.has(b.hash))queuedCount++;model.set(b.hash,b);});
|
||||
var newest=0;model.forEach(function(b){newest=Math.max(newest,b.ts);});
|
||||
// Bounded retention uses the newest observed header, not a local wall-clock guess.
|
||||
var cut=newest-340000;model.forEach(function(b,h){if(b.ts<cut)model.delete(h);});
|
||||
omitted=0;
|
||||
if(model.size>maxBlocks){var ordered=Array.from(model.values()).sort(function(a,b){return b.ts-a.ts;});omitted=model.size-maxBlocks;model=new Map(ordered.slice(0,maxBlocks).map(function(b){return[b.hash,b];}));}
|
||||
latestCps=d.finality&&Array.isArray(d.finality.checkpoints)?d.finality.checkpoints.filter(function(c){return c&&finite(c.blue_score)&&['pending','locked'].includes(c.state);}).slice(-128).map(function(c){return {index:c.index,blue_score:c.blue_score,state:c.state,fraction_total:finite(c.fraction_total)&&c.fraction_total>=0&&c.fraction_total<=1?c.fraction_total:null};}):[];
|
||||
if(d.state.stale){if(staleRight===null)staleRight=Math.min(now-lag,newest?newest+lag:now-lag);setState('stale','Observer stale'+(finite(d.state.age_s)?'; last update '+Math.round(d.state.age_s)+' s ago':''));}
|
||||
else setState('live',null);
|
||||
if(!paused)syncView();
|
||||
emit('onData',d);emit('onStats',stats());if(!paused)redraw();return true;
|
||||
}
|
||||
function stats(){
|
||||
var r=rightTime(),v=blocks.filter(function(b){return b.ts>=r-windowS*1000&&b.ts<=r;});
|
||||
return {blocks:v.length,included:v.filter(function(b){return b.color==='blue';}).length,excluded:v.filter(function(b){return b.color==='red';}).length,
|
||||
selectedChain:v.filter(function(b){return b.chain;}).length,proven:v.filter(function(b){return b.proven;}).length,
|
||||
own:v.filter(own).length,minerKeys:new Set(v.map(function(b){return b.miner;})).size,
|
||||
lockedCheckpoints:cps.filter(function(c){return c.state==='locked';}).length,paused:paused,following:following,queued:paused?queuedCount:0,
|
||||
invalidRecords:invalid,omittedRecords:omitted,rendered:blocks.length,frames:frameCount,window:windowS,reducedMotion:reduced||!manualMotion,state:state};
|
||||
}
|
||||
function poll(){
|
||||
if(destroyed||opts.poll===false||doc.hidden)return;
|
||||
if(inflight){pollAgain=true;return;}
|
||||
clearTimeout(pollTimer);inflight=true;controller=new AbortController();var signal=controller.signal;
|
||||
timeout=setTimeout(function(){if(controller)controller.abort();},finite(opts.timeoutMs)?opts.timeoutMs:8000);
|
||||
var url;
|
||||
try{url=new URL(opts.url||'/api/live',doc.baseURI);if(!['https:','http:'].includes(url.protocol))throw new Error('Use an HTTP(S) observer URL.');url.searchParams.set('window',String(Math.round(windowS)));}
|
||||
catch(e){finishError(e);return;}
|
||||
fetch(url.href,{cache:'no-store',signal:signal,credentials:'same-origin'}).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.json();}).then(function(d){if(destroyed)return;if(!push(d))throw new Error('Invalid observer reply');}).catch(finishError).finally(finish);
|
||||
function finishError(e){if(destroyed||doc.hidden)return;failures++;setState(failures>=2?'failed':'stale','Observer unavailable; retrying'+(e&&e.name==='AbortError'?' (timeout)':''));if(!url)finish();}
|
||||
function finish(){clearTimeout(timeout);timeout=0;controller=null;inflight=false;if(destroyed||opts.poll===false||doc.hidden)return;var again=pollAgain;pollAgain=false;pollTimer=setTimeout(poll,again?0:Math.min(30000,2000*Math.pow(2,Math.min(failures,4))));}
|
||||
}
|
||||
function passes(b){return filter==='all'||filter==='chain'&&b.chain||filter==='mine'&&own(b);}
|
||||
function curve(b,p){var mx=(b.x+p.x)/2;ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.bezierCurveTo(mx,p.y,mx,b.y,b.x,b.y);}
|
||||
function pointOn(b,p,t){var u=1-t,mx=(b.x+p.x)/2;return {x:u*u*u*p.x+3*u*u*t*mx+3*u*t*t*mx+t*t*t*b.x,y:u*u*u*p.y+3*u*u*t*p.y+3*u*t*t*b.y+t*t*t*b.y};}
|
||||
function lockIcon(x,y,size,color){ctx.strokeStyle=color;ctx.lineWidth=1.25;rounded(ctx,x-size*.42,y,size*.84,size*.65,1.5);ctx.stroke();ctx.beginPath();ctx.arc(x,y,size*.25,Math.PI,0);ctx.stroke();}
|
||||
function draw(t){
|
||||
if(destroyed||!W||!H)return;
|
||||
frameCount++;var rt=rightTime(),left=rt-windowS*1000,animate=!reduced&&manualMotion&&!paused&&state==='live';var clock=paused?freezeT:t;
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,W,H);ctx.fillStyle=col.bg;ctx.fillRect(0,0,W,H);
|
||||
var plotW=W-padL-padR, plotH=H-padT-padB,lh=plotH/Math.max(1,lanes.length);
|
||||
// Sparse time ticks; true horizontal coordinate is header time, not DAA spacing.
|
||||
ctx.lineWidth=1;ctx.setLineDash([2,7]);
|
||||
for(var g=0;g<=6;g++){var gx=padL+plotW*g/6;ctx.strokeStyle=rgba(col.line,.52);ctx.beginPath();ctx.moveTo(gx,padT-10);ctx.lineTo(gx,H-padB);ctx.stroke();}
|
||||
ctx.setLineDash([]);
|
||||
lanes.forEach(function(lane,i){var y=padT+(i+.5)*lh, ownLane=blocks.some(function(b){return b.miner===lane&&own(b);});
|
||||
if(ownLane){var grad=ctx.createLinearGradient(padL,0,W,0);grad.addColorStop(0,rgba(col.molten,.085));grad.addColorStop(1,rgba(col.molten,.005));ctx.fillStyle=grad;rounded(ctx,padL-5,y-lh*.4,plotW+5,lh*.8,7);ctx.fill();}
|
||||
ctx.strokeStyle=rgba(ownLane?col.molten:col.line,ownLane?.20:.65);ctx.beginPath();ctx.moveTo(padL,y);ctx.lineTo(W-padR,y);ctx.stroke();
|
||||
if(padL>50){ctx.font='500 10px ui-monospace, SFMono-Regular, Consolas, monospace';ctx.textBaseline='middle';ctx.textAlign='left';ctx.fillStyle=ownLane?col.molten:col.ash;
|
||||
ctx.fillText(ownLane?'YOUR KEY':lane==='__others__'?'OTHERS':lane.slice(0,8),12,y-5);
|
||||
ctx.font='8px ui-monospace, monospace';ctx.fillStyle=rgba(col.ash,.78);ctx.fillText(ownLane?'YOUR BLOCKS':lane==='__others__'?'GROUPED KEYS':'MINER KEY',12,y+10);
|
||||
}
|
||||
});
|
||||
// positions
|
||||
blocks.forEach(function (b) { b.x = xOf(b.ts); b.y = laneY(b); b.on = b.ts >= vis && b.x >= padL - 10; });
|
||||
// edges: merge edges thin, chain-to-chain heavier; the chain as one glowing path
|
||||
blocks.forEach(function (b) {
|
||||
if (!b.on) return;
|
||||
b.parents.forEach(function (h) {
|
||||
var p = byHash[h]; if (!p || !p.on) return;
|
||||
var chainEdge = b.chain && p.chain;
|
||||
ctx.strokeStyle = chainEdge ? rgba(COL.ember, 0.75) : rgba(COL.ash, b.color === 'red' ? 0.12 : 0.22);
|
||||
ctx.lineWidth = chainEdge ? 2 : 1;
|
||||
ctx.beginPath(); ctx.moveTo(b.x, b.y); var mx = (b.x + p.x) / 2; ctx.bezierCurveTo(mx, b.y, mx, p.y, p.x, p.y); ctx.stroke();
|
||||
if (chainEdge) { ctx.strokeStyle = rgba(COL.ember, 0.18); ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.bezierCurveTo(mx, b.y, mx, p.y, p.x, p.y); ctx.stroke(); }
|
||||
});
|
||||
var cpLabels=[];
|
||||
cps.forEach(function(c){
|
||||
// Exact score only. Never pin a checkpoint to an unrelated nearest block.
|
||||
var candidates=blocks.filter(function(b){return b.blue_score===c.blue_score;});
|
||||
var ref=candidates.find(function(b){return b.locked;})||candidates.find(function(b){return b.chain;})||(candidates.length===1?candidates[0]:null);
|
||||
if(!ref||ref.ts<left||ref.ts>rt)return;
|
||||
var x=xOf(ref.ts,rt),locked=c.state==='locked',burstAt=checkpointBursts.get(c.index),burst=animate&&burstAt?Math.max(0,1-(t-burstAt)/1400):0;
|
||||
var band=ctx.createLinearGradient(x-20,0,x+36,0);band.addColorStop(0,rgba(col.ember,0));band.addColorStop(.38,rgba(col.ember,locked?.09:.035));band.addColorStop(1,rgba(col.ember,0));ctx.fillStyle=band;ctx.fillRect(x-20,padT-12,56,plotH+12);
|
||||
ctx.strokeStyle=rgba(col.ember,locked?.6:.32);ctx.lineWidth=locked?1.5:1;ctx.setLineDash(locked?[]:[3,6]);ctx.beginPath();ctx.moveTo(x,padT-14);ctx.lineTo(x,H-padB);ctx.stroke();ctx.setLineDash([]);
|
||||
if(burst>0){ctx.strokeStyle=rgba(col.molten,burst*.5);ctx.lineWidth=1;ctx.strokeRect(x-(1-burst)*42,padT-12,(1-burst)*84,plotH+12);}
|
||||
if(!compact && W>510)cpLabels.push({x:x,cp:c,locked:locked});
|
||||
});
|
||||
// blocks
|
||||
var S = compact ? 9 : 8, now = performance.now();
|
||||
blocks.forEach(function (b) {
|
||||
if (!b.on) return;
|
||||
var age = now - b.born, glow = age < 1600 ? 1 - age / 1600 : 0, ex = b.color === 'red';
|
||||
var r = b.chain ? S + 1 : S;
|
||||
if (glow > 0) { var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, r * 3); g.addColorStop(0, rgba(COL.ember, 0.4 * glow)); g.addColorStop(1, rgba(COL.ember, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, r * 3, 0, Math.PI * 2); ctx.fill(); }
|
||||
ctx.globalAlpha = ex ? 0.45 : 1;
|
||||
// fill: proven = ember, otherwise the row surface; outline: included (blue or chain) = ember, pending = grey, excluded = dim
|
||||
var proving = !b.proven && b.shards.some(function (s) { return s.state === 'proving' || s.state === 'verified' || s.state === 'paid'; });
|
||||
ctx.beginPath(); rr(b.x - r, b.y - r, 2 * r, 2 * r, 3);
|
||||
ctx.fillStyle = b.proven ? COL.ember : COL.obsidian; ctx.fill();
|
||||
ctx.lineWidth = b.chain ? 2 : 1.5;
|
||||
// chain blocks in ember (the path), included blocks lit in bone, pending in ash, excluded dim
|
||||
ctx.strokeStyle = b.proven ? COL.ember : proving ? COL.molten : b.chain ? rgba(COL.ember, 0.9) : b.color === 'blue' ? rgba(COL.bone, 0.8) : ex ? rgba(COL.ash, 0.5) : rgba(COL.ash, 0.75);
|
||||
ctx.stroke();
|
||||
if (b.locked) { ctx.strokeStyle = rgba(COL.ember, 0.8 + 0.2 * Math.sin(t / 350)); ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(b.x, b.y, r * 1.9, 0, Math.PI * 2); ctx.stroke(); }
|
||||
if (isMine && isMine(b)) { ctx.strokeStyle = rgba(COL.molten, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(b.x, b.y, r * 1.5, 0, Math.PI * 2); ctx.stroke(); }
|
||||
ctx.globalAlpha = 1;
|
||||
cpLabels.sort(function(a,b){return b.x-a.x;});var cpLast=Infinity;
|
||||
cpLabels.forEach(function(p){var label=(p.locked?'LOCKED':'PENDING')+(p.cp.fraction_total===null?'':' '+Math.round(p.cp.fraction_total*100)+'%'),w=label.length*5.5+22,x=clamp(p.x-w/2,padL,W-padR-w);if(x+w+8>cpLast)return;cpLast=x;
|
||||
ctx.fillStyle=col.bg;ctx.strokeStyle=rgba(col.ember,p.locked?.45:.23);rounded(ctx,x,17,w,22,4);ctx.fill();ctx.stroke();ctx.font='9px ui-monospace, monospace';ctx.fillStyle=p.locked?col.ember:col.ash;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(label,x+w/2,28);
|
||||
});
|
||||
// hover or pinned ring
|
||||
var f = pinned || hover; if (f && f.on) { ctx.strokeStyle = COL.bone; ctx.lineWidth = 1; ctx.setLineDash([2, 3]); ctx.beginPath(); ctx.arc(f.x, f.y, S * 2.4, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); }
|
||||
// axis in DAA at the left and right edges, and the lock line label
|
||||
if (!compact) {
|
||||
ctx.fillStyle = COL.ash; ctx.font = '400 10px IBM Plex Mono, monospace'; ctx.textBaseline = 'alphabetic';
|
||||
var first = null, last = null; blocks.forEach(function (b) { if (!b.on) return; if (!first || b.ts < first.ts) first = b; if (!last || b.ts > last.ts) last = b; });
|
||||
if (first && last) { ctx.textAlign = 'left'; ctx.fillText('daa ' + fmt(first.daa), padL, padT - 8); ctx.textAlign = 'right'; ctx.fillText('daa ' + fmt(last.daa) + (paused ? ' paused' : ''), W - padR, padT - 8); }
|
||||
// Animate lane transitions without changing their underlying key identity.
|
||||
blocks.forEach(function(b){b.x=xOf(b.ts,rt);var progress=animate?clamp((clock-b.laneAt)/450,0,1):1;progress=1-Math.pow(1-progress,3);b.y=b.fromY+(b.targetY-b.fromY)*progress;b.on=b.ts>=left&&b.ts<=rt&&passes(b);if(b.on&&b.newArrival&&b.visibleAt===null){b.visibleAt=t;b.born=t;}});
|
||||
ctx.save();ctx.beginPath();ctx.rect(padL-1,padT-15,plotW+2,plotH+25);ctx.clip();
|
||||
blocks.forEach(function(b){if(!b.on)return;var bSel=selected&&b.hash===selected;b.parents.forEach(function(hash,pi){var p=view.get(hash);if(!p||!passes(p)||p.x>W-padR||p.x<padL-plotW*.2)return;
|
||||
var path=b.chain&&p.chain,selectedEdge=selected&&(b.hash===selected||p.hash===selected);
|
||||
if(!path&&!selectedEdge){if(pi>0)return;var li=laneOf.has(b.miner)?laneOf.get(b.miner):lanes.length-1,lp=laneOf.has(p.miner)?laneOf.get(p.miner):lanes.length-1;if(Math.abs(li-lp)>laneReach)return;}
|
||||
if(path||selectedEdge){ctx.strokeStyle=rgba(path?col.ember:col.bone,path?.1:.10);ctx.lineWidth=path?8:4;curve(b,p);ctx.stroke();}
|
||||
ctx.strokeStyle=rgba(path?col.ember:selectedEdge?col.bone:b.color==='red'?col.red:col.ash,path?.68:selectedEdge?.7:hairAlpha);ctx.lineWidth=path?1.65:selectedEdge?1.4:1;curve(b,p);ctx.stroke();
|
||||
var age=t-b.born;if(animate&&age>=0&&age<1400){var f=clamp(age/1150,0,1),pt=pointOn(b,p,f);ctx.shadowBlur=10;ctx.shadowColor=path?col.ember:col.blue;ctx.fillStyle=path?col.molten:col.blue;ctx.beginPath();ctx.arc(pt.x,pt.y,path?2.2:1.65,0,TAU);ctx.fill();ctx.shadowBlur=0;}
|
||||
});});
|
||||
var S=compact?6.5:clamp(lh*.22,minNode,13);
|
||||
blocks.forEach(function(b){if(!b.on)return;
|
||||
var age=t-b.born,newness=animate?Math.max(clamp(1-age/1700,0,1),clamp(1-(t-b.changed)/900,0,1)*.65):0;
|
||||
var ink=b.color==='red'?col.red:b.chain?col.ember:b.color==='blue'?col.blue:col.ash;
|
||||
var isOwn=own(b),isSelected=b.hash===selected||b.hash===hover,inOthers=!laneOf.has(b.miner),sz=S*(1+(newness*.15))*(inOthers?othersScale:1);
|
||||
if(newness>0||isOwn||isSelected){var glow=ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,sz*3.8);glow.addColorStop(0,rgba(isOwn?col.molten:ink,newness*.26+(isSelected?.15:isOwn?.06:0)));glow.addColorStop(1,rgba(ink,0));ctx.fillStyle=glow;ctx.fillRect(b.x-sz*4,b.y-sz*4,sz*8,sz*8);}
|
||||
ctx.globalAlpha=(b.color==='red'?.78:1)*(inOthers&&!isSelected&&!isOwn?othersScale:1);
|
||||
// a proven block glows and carries a ring, so the proof reads from across the room
|
||||
if(provenGlow&&b.proven){var pg=ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,sz*3.2);pg.addColorStop(0,rgba(ink,.26));pg.addColorStop(1,rgba(ink,0));ctx.fillStyle=pg;ctx.fillRect(b.x-sz*3.4,b.y-sz*3.4,sz*6.8,sz*6.8);ctx.strokeStyle=rgba(ink,.6);ctx.lineWidth=1;ctx.beginPath();ctx.arc(b.x,b.y,sz+5,0,TAU);ctx.stroke();}
|
||||
// Inset tiles: inclusion is the outline; proof is the core, never the same status.
|
||||
ctx.fillStyle=col.bg;ctx.strokeStyle=rgba(ink,b.chain?.95:.8);ctx.lineWidth=b.chain?1.75:1.25;rounded(ctx,b.x-sz,b.y-sz,2*sz,2*sz,3);ctx.fill();ctx.stroke();
|
||||
var tile=ctx.createLinearGradient(b.x,b.y-sz,b.x,b.y+sz);tile.addColorStop(0,rgba(ink,b.proven?.5:.16));tile.addColorStop(1,rgba(ink,b.proven?.22:.025));ctx.fillStyle=tile;rounded(ctx,b.x-sz+2,b.y-sz+2,2*sz-4,2*sz-4,2);ctx.fill();
|
||||
if(b.proven){ctx.strokeStyle=rgba(b.color==='red'?col.red:col.bone,.9);ctx.lineWidth=1.3;ctx.beginPath();ctx.moveTo(b.x-3,b.y);ctx.lineTo(b.x-1,b.y+2);ctx.lineTo(b.x+3,b.y-2);ctx.stroke();}
|
||||
else if(b.color==='red'){ctx.strokeStyle=ink;ctx.lineWidth=1.1;ctx.beginPath();ctx.moveTo(b.x-2,b.y-2);ctx.lineTo(b.x+2,b.y+2);ctx.moveTo(b.x+2,b.y-2);ctx.lineTo(b.x-2,b.y+2);ctx.stroke();}
|
||||
else{ctx.fillStyle=rgba(ink,.65);ctx.fillRect(b.x-1.3,b.y-1.3,2.6,2.6);}
|
||||
if(!compact && b.shards.length){var total=b.shards.length,shown=Math.min(total,8),gap=1.6,barW=2*sz,seg=(barW-gap*(shown-1))/shown;
|
||||
for(var k=0;k<shown;k++){var shard=b.shards[k].state,alpha=shard==='proving'?(animate?.5+.4*Math.sin(t/300+k):.8):1;
|
||||
ctx.fillStyle=rgba(shard==='paid'?col.molten:shard==='verified'?col.bone:shard==='proving'?col.ember:col.line2,alpha);ctx.fillRect(b.x-sz+k*(seg+gap),b.y+sz+4,Math.max(1,seg),2);
|
||||
}
|
||||
}
|
||||
if(isOwn){ctx.strokeStyle=col.molten;ctx.lineWidth=1.2;var q=sz+4;ctx.beginPath();[[1,1],[1,-1],[-1,1],[-1,-1]].forEach(function(v){ctx.moveTo(b.x+v[0]*(q-4),b.y+v[1]*q);ctx.lineTo(b.x+v[0]*q,b.y+v[1]*q);ctx.lineTo(b.x+v[0]*q,b.y+v[1]*(q-4));});ctx.stroke();}
|
||||
if(b.locked){ctx.strokeStyle=rgba(col.ember,.6);ctx.lineWidth=1;ctx.beginPath();ctx.arc(b.x,b.y,sz+8,0,TAU);ctx.stroke();}
|
||||
if(b.final){ctx.fillStyle=rgba(col.ember,.75);ctx.beginPath();ctx.arc(b.x+sz-1,b.y-sz-2,2,0,TAU);ctx.fill();}
|
||||
if(isSelected){ctx.strokeStyle=col.bone;ctx.lineWidth=1;ctx.setLineDash([3,3]);rounded(ctx,b.x-sz-8,b.y-sz-8,2*sz+16,2*sz+16,6);ctx.stroke();ctx.setLineDash([]);}
|
||||
ctx.globalAlpha=1;
|
||||
});ctx.restore();
|
||||
// Timeline and feed edge. No fabricated DAA ticks or estimated checkpoint positions.
|
||||
ctx.font=(compact?'8':'9')+'px ui-monospace, monospace';ctx.fillStyle=col.ash;ctx.textBaseline='middle';
|
||||
var ticks=W<420?2:4;
|
||||
for(var q=0;q<=ticks;q++){var tx=padL+plotW*q/ticks;ctx.textAlign=q===0?'left':q===ticks?'right':'center';var label=q===ticks?(paused?'PAUSED':!following?'HISTORY':state==='live'?'OBSERVED NOW':state.toUpperCase()):'-'+Math.round(windowS*(1-q/ticks))+'s';ctx.fillText(label,tx,H-(compact?8:13));}
|
||||
if(!compact){var visibleBlocks=blocks.filter(function(b){return b.on;});ctx.font='8px ui-monospace, monospace';ctx.textAlign='left';ctx.fillStyle=rgba(col.ash,.8);if(padL>50)ctx.fillText('HEADER TIME',12,H-13);
|
||||
if(visibleBlocks.length && W>600){var last=visibleBlocks[visibleBlocks.length-1];ctx.textAlign='right';ctx.fillText('DAA '+fmt(last.daa),W-padR,8);}
|
||||
}
|
||||
if (paused) { ctx.fillStyle = rgba(COL.obsidian, 0.0); }
|
||||
if(state==='live'&&following&&!paused){ctx.strokeStyle=rgba(col.ember,.3);ctx.beginPath();ctx.moveTo(W-padR,padT-9);ctx.lineTo(W-padR,H-padB);ctx.stroke();ctx.fillStyle=col.ember;ctx.beginPath();ctx.arc(W-padR,padT-12,2.3,0,TAU);ctx.fill();}
|
||||
if(!blocks.some(function(b){return b.on;})){
|
||||
ctx.textAlign='center';ctx.fillStyle=col.ash;ctx.font='12px system-ui, sans-serif';ctx.fillText(state==='connecting'?'Waiting for observer data':filter==='mine'?'No blocks for your key in this window':filter==='chain'?'No selected-chain blocks in this window':'No blocks in this time window',padL+plotW/2,H/2);
|
||||
}
|
||||
var active=selected?view.get(selected):hover?view.get(hover):null;if(active&&active.on)showTip(active);else if(tip)tip.hidden=true;
|
||||
lastT=t;
|
||||
}
|
||||
function rr(x, y, w, h, r) { ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
|
||||
|
||||
function frame(t) { if (!running) return; if (!paused) draw(t); if (reduce || document.hidden || !inView) { running = false; return; } requestAnimationFrame(frame); }
|
||||
function start() { if (running) return; running = true; requestAnimationFrame(frame); }
|
||||
function kick() { if (!running) requestAnimationFrame(function (t) { draw(t); if (!reduce && !document.hidden && inView) start(); }); }
|
||||
|
||||
// ---- interaction ----
|
||||
function hit(ev) {
|
||||
var rect = canvas.getBoundingClientRect(), px = ev.clientX - rect.left, py = ev.clientY - rect.top, best = null, bd = 14 * 14;
|
||||
blocks.forEach(function (b) { if (!b.on) return; var dx = b.x - px, dy = b.y - py, d = dx * dx + dy * dy; if (d < bd) { bd = d; best = b; } });
|
||||
return best;
|
||||
function continuous(){return !destroyed&&!doc.hidden&&visible&&!paused&&!reduced&&manualMotion&&state==='live'&&blocks.length>0;}
|
||||
function frame(t){raf=0;if(destroyed||doc.hidden||!visible)return;if(t-lastPaint>=minFrame-.5){draw(t);lastPaint=t;}if(continuous())raf=requestAnimationFrame(frame);}
|
||||
function redraw(){if(destroyed||raf||doc.hidden||!visible)return;lastPaint=-Infinity;raf=requestAnimationFrame(frame);}
|
||||
function stopFrame(){if(raf)cancelAnimationFrame(raf);raf=0;}
|
||||
function words(b){
|
||||
if(!b)return {title:'No block selected',lines:[]};
|
||||
var inclusion=b.color==='blue'?'included':b.color==='red'?'excluded':'pending';
|
||||
var bits=[inclusion];if(b.chain)bits.push('selected chain');if(b.proven)bits.push('proven');if(b.locked)bits.push('locked checkpoint');if(b.final)bits.push('final');
|
||||
return {title:b.hash+' '+bits.join(' / '),lines:[(own(b)?'Your block / ':'')+'miner key '+b.miner,
|
||||
'Blue score '+fmt(b.blue_score===undefined?b.blue:b.blue_score)+' / DAA '+fmt(b.daa),b.parents.length+' parent'+(b.parents.length===1?'':'s'),
|
||||
'Shards: '+(b.shards.length?b.shards.map(function(s){return s.state;}).join(', '):'no shard plan yet')]};
|
||||
}
|
||||
function words(b) {
|
||||
var state = b.locked ? 'locked checkpoint' : b.proven ? 'proven' : b.chain ? 'included, selected chain' : b.color === 'blue' ? 'included' : b.color === 'red' ? 'excluded' : 'pending';
|
||||
var sh = b.shards.length ? b.shards.map(function (s) { return s.state; }).join(', ') : 'no shard plan yet';
|
||||
return { title: b.hash + ' ' + state, lines: [(isMine && isMine(b) ? 'your block, miner ' : 'miner ') + b.miner + (b.miner in laneOf ? '' : ' (others lane)'), 'blue score ' + fmt(b.blue) + ' daa ' + fmt(b.daa), b.parents.length + (b.parents.length === 1 ? ' parent' : ' parents'), 'proof: ' + sh + (b.final ? ' final' : '')] };
|
||||
function showTip(b){
|
||||
if(!tip)return;
|
||||
if(!b){tip.hidden=true;return;}
|
||||
var w=words(b),key=w.title+'|'+w.lines.join('|');
|
||||
if(tip.dataset.igneumKey!==key){tip.replaceChildren();var title=doc.createElement('b');title.textContent=w.title;tip.appendChild(title);w.lines.forEach(function(line){var p=doc.createElement('span');p.textContent=line;p.style.display='block';tip.appendChild(p);});tip.dataset.igneumKey=key;}
|
||||
tip.hidden=false;tip.style.pointerEvents='none';tip.style.position='absolute';tip.style.left='0px';tip.style.top='0px';tip.style.maxWidth=Math.max(160,W-24)+'px';tip.style.whiteSpace='normal';tip.style.overflowWrap='anywhere';
|
||||
var host=tip.offsetParent||canvas.parentElement,cr=canvas.getBoundingClientRect(),pr=host.getBoundingClientRect();
|
||||
var tw=tip.offsetWidth,th=tip.offsetHeight;var x=clamp(b.x+16,8,Math.max(8,W-tw-8)),y=b.y-th-18;if(y<8)y=Math.min(H-th-8,b.y+22);
|
||||
tip.style.transform='translate('+Math.round(cr.left-pr.left-host.clientLeft+x)+'px,'+Math.round(cr.top-pr.top-host.clientTop+Math.max(8,y))+'px)';
|
||||
}
|
||||
var tip = opts.tooltip || null;
|
||||
function showTip(b, ev) {
|
||||
if (!tip) return;
|
||||
if (!b) { tip.hidden = true; return; }
|
||||
var w = words(b); tip.hidden = false; tip.innerHTML = '<b>' + w.title + '</b>' + w.lines.map(function (l) { return '<span>' + l + '</span>'; }).join('');
|
||||
var rect = canvas.getBoundingClientRect(); var x = Math.min(rect.width - tip.offsetWidth - 8, Math.max(8, b.x + 12)), y = Math.max(8, b.y - tip.offsetHeight - 10); if (y < 8) y = b.y + 14;
|
||||
tip.style.transform = 'translate(' + Math.round(x) + 'px,' + Math.round(y) + 'px)';
|
||||
function hit(ev){var p=eventXY(ev),best=null,dist=22*22;blocks.forEach(function(b){if(!b.on)return;var d=(b.x-p.x)**2+(b.y-p.y)**2;if(d<dist){dist=d;best=b;}});return best;}
|
||||
function select(hash,why){if(hash!==null&&!view.has(hash))return false;selected=hash;showTip(hash?view.get(hash):null);emit('onSelect',hash?cloneBlock(view.get(hash)):null,why||'select');redraw();return true;}
|
||||
function engage(onValue){var value=!!onValue;if(engaged===value)return;engaged=value;canvas.classList.toggle('engaged',value);if(chip)chip.hidden=!value;emit('onEngage',value);}
|
||||
function setWindow(seconds){var n=Math.round(clamp(seconds,30,300));if(!finite(n)||n===windowS)return;windowS=n;layout();emit('onWindow',n);emit('onStats',stats());poll();redraw();}
|
||||
function zoom(f){if(!finite(f)||f<=0)return;setWindow(windowS*f);}
|
||||
function follow(){following=true;fixedRight=0;if(paused)pausedRight=Date.now()-lag;layout();emit('onFollow',true);redraw();}
|
||||
function setPaused(p){p=!!p;if(paused===p)return;
|
||||
if(p){pausedRight=rightTime();freezeT=performance.now();paused=true;stopFrame();}
|
||||
else{paused=false;syncView();}
|
||||
if(opts.pauseButton){opts.pauseButton.setAttribute('aria-pressed',String(paused));opts.pauseButton.textContent=paused?'Resume':'Pause';}
|
||||
emit('onPause',paused);emit('onStats',stats());redraw();
|
||||
}
|
||||
canvas.addEventListener('mousemove', function (ev) { hover = hit(ev); canvas.style.cursor = hover ? 'pointer' : 'default'; if (!pinned) showTip(hover, ev); kick(); });
|
||||
canvas.addEventListener('mouseleave', function () { hover = null; if (!pinned) showTip(null); kick(); });
|
||||
canvas.addEventListener('click', function (ev) { engage(true); var b = hit(ev); pinned = (pinned && b && pinned.hash === b.hash) ? null : b; showTip(pinned || hover); kick(); });
|
||||
// the wheel is never captured by default (6 Oct 2026, the owner: scrolling past the scene zoomed it): the page scrolls through the
|
||||
// scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene, which engages it (opts.chip shows
|
||||
// "scroll to zoom, Esc to release"); Escape, a click outside or leaving the scene releases it
|
||||
var engaged = false, chip = opts.chip || null;
|
||||
function engage(on) { if (on === engaged) return; engaged = on; canvas.classList.toggle('engaged', on); if (chip) chip.hidden = !on; if (opts.onEngage) opts.onEngage(on); }
|
||||
canvas.addEventListener('wheel', function (ev) {
|
||||
var zoomKey = ev.ctrlKey || ev.metaKey;
|
||||
if (!zoomKey && !engaged) return; // a plain wheel or a two-finger scroll passes through to the page
|
||||
if (Math.abs(ev.deltaY) < 1) return;
|
||||
ev.preventDefault(); zoom(ev.deltaY > 0 ? 1.15 : 1 / 1.15);
|
||||
}, { passive: false });
|
||||
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape' && engaged) engage(false); });
|
||||
document.addEventListener('pointerdown', function (ev) { if (engaged && !canvas.contains(ev.target) && !(chip && chip.contains(ev.target))) engage(false); });
|
||||
var pinch = null;
|
||||
canvas.addEventListener('touchstart', function (ev) { if (ev.touches.length === 2) pinch = dist(ev.touches); }, { passive: true });
|
||||
canvas.addEventListener('touchmove', function (ev) { if (ev.touches.length === 2 && pinch) { var d = dist(ev.touches); if (Math.abs(d - pinch) > 12) { zoom(d > pinch ? 1 / 1.1 : 1.1); pinch = d; } } }, { passive: true });
|
||||
canvas.addEventListener('touchend', function () { pinch = null; }, { passive: true });
|
||||
function dist(t) { var dx = t[0].clientX - t[1].clientX, dy = t[0].clientY - t[1].clientY; return Math.sqrt(dx * dx + dy * dy); }
|
||||
function zoom(f) { var w = Math.round(Math.max(30, Math.min(300, windowS * f))); if (w === windowS) return; windowS = w; layout(); if (opts.onWindow) opts.onWindow(w); poll(); kick(); }
|
||||
function setPaused(p) { paused = !!p; if (paused) { offsetMs = 0; pausedAt = Date.now(); } else { offsetMs = 0; } if (opts.pauseButton) { opts.pauseButton.setAttribute('aria-pressed', paused ? 'true' : 'false'); opts.pauseButton.textContent = paused ? 'Resume' : 'Pause'; } kick(); }
|
||||
var pausedAt = 0;
|
||||
if (opts.pauseButton) opts.pauseButton.addEventListener('click', function () { setPaused(!paused); });
|
||||
// while paused the clock stops: offsetMs grows so the scene holds still but keeps its blocks
|
||||
setInterval(function () { if (paused) offsetMs = Date.now() - pausedAt; }, 250);
|
||||
|
||||
if ('IntersectionObserver' in root) { new IntersectionObserver(function (es) { es.forEach(function (e) { inView = e.isIntersecting; if (inView) kick(); }); }, { threshold: 0.05 }).observe(canvas); }
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) { poll(); kick(); } });
|
||||
if (root.matchMedia) { try { root.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { readTheme(); kick(); }); } catch (e) { } }
|
||||
new MutationObserver(function () { readTheme(); kick(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
|
||||
readTheme(); size(); if (opts.poll !== false) { poll(); setInterval(poll, 2000); } kick();
|
||||
return { push: push, setPaused: setPaused, zoom: zoom, engage: engage, getWindow: function () { return windowS; }, getState: function () { return { state: state, reason: reason }; }, redraw: kick, words: words };
|
||||
function setFilter(value){if(!['all','chain','mine'].includes(value))return;filter=value;select(null,'filter');layout();redraw();}
|
||||
function setMotion(value){manualMotion=!!value;reducedRight=Date.now()-lag;stopFrame();redraw();}
|
||||
function panTo(right){following=false;fixedRight=Math.min(right,Date.now()-lag);if(paused)pausedRight=fixedRight;emit('onFollow',false);layout();redraw();}
|
||||
canvas.setAttribute('tabindex',canvas.getAttribute('tabindex')||'0');canvas.setAttribute('role','img');
|
||||
if(!canvas.getAttribute('aria-label'))canvas.setAttribute('aria-label','Interactive block graph. Arrow keys inspect blocks. Plus and minus zoom. Space pauses, F follows, Escape releases.');
|
||||
canvas.style.touchAction='pan-y';
|
||||
on(canvas,'pointerdown',function(ev){if(ev.pointerType==='touch'||ev.button!==0)return;canvas.focus({preventScroll:true});engage(true);drag={id:ev.pointerId,x:ev.clientX,start:rightTime(),moved:false};canvas.setPointerCapture(ev.pointerId);});
|
||||
on(canvas,'pointermove',function(ev){
|
||||
if(drag&&drag.id===ev.pointerId){var dx=ev.clientX-drag.x;if(Math.abs(dx)>4)drag.moved=true;if(drag.moved){panTo(drag.start-dx*windowS*1000/Math.max(1,W-padL-padR));canvas.style.cursor='grabbing';return;}}
|
||||
var b=hit(ev);hover=b?b.hash:null;canvas.style.cursor=b?'pointer':'grab';if(!selected)showTip(b);if(!continuous())redraw();
|
||||
});
|
||||
on(canvas,'pointerup',function(ev){if(ev.pointerType==='touch')return;if(!drag||drag.id!==ev.pointerId)return;var moved=drag.moved;drag=null;if(canvas.hasPointerCapture(ev.pointerId))canvas.releasePointerCapture(ev.pointerId);canvas.style.cursor='grab';if(!moved){var b=hit(ev);select(b&&selected!==b.hash?b.hash:null,'pointer');}});
|
||||
on(canvas,'pointercancel',function(){drag=null;});
|
||||
on(canvas,'pointerleave',function(){hover=null;if(!drag)engage(false);if(!selected)showTip(null);redraw();});
|
||||
on(canvas,'wheel',function(ev){if(!ev.ctrlKey&&!ev.metaKey&&!engaged)return;if(Math.abs(ev.deltaY)<1)return;ev.preventDefault();zoom(ev.deltaY>0?1.15:1/1.15);},{passive:false});
|
||||
var touchStart=null;
|
||||
function dist(touches){return Math.hypot(touches[0].clientX-touches[1].clientX,touches[0].clientY-touches[1].clientY);}
|
||||
on(canvas,'touchstart',function(ev){if(ev.touches.length===2){pinch={distance:dist(ev.touches),window:windowS};touchStart=null;}else if(ev.touches.length===1){touchStart={x:ev.touches[0].clientX,y:ev.touches[0].clientY};}},{passive:true});
|
||||
on(canvas,'touchmove',function(ev){if(ev.touches.length===2&&pinch){ev.preventDefault();setWindow(pinch.window*pinch.distance/Math.max(1,dist(ev.touches)));}else if(touchStart&&ev.touches.length){if(Math.hypot(ev.touches[0].clientX-touchStart.x,ev.touches[0].clientY-touchStart.y)>8)touchStart=null;}},{passive:false});
|
||||
on(canvas,'touchend',function(ev){if(pinch){if(ev.touches.length<2)pinch=null;touchStart=null;return;}if(touchStart&&ev.changedTouches.length){var b=hit(ev.changedTouches[0]);select(b&&selected!==b.hash?b.hash:null,'touch');touchStart=null;}},{passive:true});
|
||||
on(canvas,'keydown',function(ev){
|
||||
var key=ev.key;if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(key)){
|
||||
ev.preventDefault();var list=blocks.filter(function(b){return b.on;}),i=list.findIndex(function(b){return b.hash===selected;}),next;
|
||||
if(!list.length)return;if(key==='Home')next=0;else if(key==='End')next=list.length-1;else next=i<0?list.length-1:clamp(i+(['ArrowRight','ArrowDown'].includes(key)?1:-1),0,list.length-1);select(list[next].hash,'keyboard');
|
||||
}else if(key===' '){ev.preventDefault();setPaused(!paused);}else if(key==='+'||key==='='){ev.preventDefault();zoom(1/1.2);}else if(key==='-'){ev.preventDefault();zoom(1.2);}else if(key.toLowerCase()==='f'){ev.preventDefault();follow();}
|
||||
});
|
||||
on(doc,'keydown',function(ev){if(ev.key==='Escape'&&(engaged||doc.activeElement===canvas)){engage(false);select(null,'escape');}});
|
||||
on(doc,'pointerdown',function(ev){if(engaged&&ev.target!==canvas&&!(chip&&chip.contains(ev.target)))engage(false);});
|
||||
if(opts.pauseButton){opts.pauseButton.setAttribute('aria-pressed','false');on(opts.pauseButton,'click',function(){setPaused(!paused);});}
|
||||
on(root,'resize',size);
|
||||
if('ResizeObserver'in root){ro=new ResizeObserver(size);ro.observe(canvas);}
|
||||
if('IntersectionObserver'in root){io=new IntersectionObserver(function(es){visible=es[0].isIntersecting;if(visible)redraw();else stopFrame();},{threshold:0});io.observe(canvas);}
|
||||
if('MutationObserver'in root){mo=new MutationObserver(theme);mo.observe(doc.documentElement,{attributes:true,attributeFilter:['data-theme','class','style']});}
|
||||
if(mq&&mq.addEventListener)on(mq,'change',function(e){reduced=e.matches;reducedRight=Date.now()-lag;stopFrame();redraw();});
|
||||
if(mediaTheme&&mediaTheme.addEventListener)on(mediaTheme,'change',theme);
|
||||
on(doc,'visibilitychange',function(){if(doc.hidden){stopFrame();clearTimeout(pollTimer);if(controller)controller.abort();}else{poll();redraw();}});
|
||||
healthTimer=setInterval(function(){if(destroyed||doc.hidden||!lastGood)return;if(state==='live'&&Date.now()-lastGood>(finite(opts.staleAfterMs)?opts.staleAfterMs:15000))setState('stale','No observer update for '+Math.round((Date.now()-lastGood)/1000)+' s');},1000);
|
||||
function destroy(){if(destroyed)return;destroyed=true;stopFrame();clearTimeout(pollTimer);clearTimeout(timeout);clearInterval(healthTimer);if(controller)controller.abort();remove.forEach(function(fn){fn();});if(ro)ro.disconnect();if(io)io.disconnect();if(mo)mo.disconnect();if(tip)tip.hidden=true;canvas.classList.remove('engaged');
|
||||
[['tabindex',initialAttributes.tabindex],['role',initialAttributes.role],['aria-label',initialAttributes.label]].forEach(function(pair){if(pair[1]===null)canvas.removeAttribute(pair[0]);else canvas.setAttribute(pair[0],pair[1]);});canvas.style.touchAction=initialAttributes.touch;canvas.style.cursor=initialAttributes.cursor;instances.delete(canvas);model.clear();view.clear();blocks=[];
|
||||
}
|
||||
var api={push:push,setPaused:setPaused,zoom:zoom,engage:engage,getWindow:function(){return windowS;},getState:function(){return {state:state,reason:reason};},redraw:redraw,words:words,
|
||||
destroy:destroy,follow:follow,setWindow:setWindow,setFilter:setFilter,setMotion:setMotion,select:select,getStats:stats,
|
||||
getViewRange:function(){return {start:rightTime()-windowS*1000,end:rightTime(),filter:filter};},getBlocks:function(){return blocks.map(cloneBlock);},getCheckpoints:function(){return cps.map(function(c){return Object.assign({},c);});},
|
||||
getSnapshot:function(){return {ok:true,state:{stale:state!=='live'},blocks:blocks.map(cloneBlock),finality:{checkpoints:cps.map(function(c){return Object.assign({},c);})}};}};
|
||||
instances.set(canvas,api);if(chip)chip.hidden=true;theme();size();emit('onState',state,reason);if(opts.poll!==false)poll();redraw();return api;
|
||||
}
|
||||
root.IgneumDag = { mount: mount };
|
||||
root.IgneumDag={mount:mount,version:'2.0.1'};
|
||||
})(window);
|
||||
|
|
|
|||
499
site/live.html
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Igneum live devnet</title>
|
||||
<meta name="description" content="The Igneum devnet as it happens: every block, the miners, the provers paid and the finality rule, read from a node every two seconds.">
|
||||
<meta name="description" content="The Igneum network observatory: every block and its connections, the miners, the provers paid and the finality rule, read from a node every two seconds.">
|
||||
<link rel="canonical" href="https://igneum.network/live">
|
||||
<meta name="theme-color" content="#0C0C0E">
|
||||
<meta property="og:type" content="website">
|
||||
|
|
@ -53,54 +53,149 @@
|
|||
</script>
|
||||
<!-- head:end -->
|
||||
<style>
|
||||
/* live page block (7 Oct 2026: redrawn to show the system working); tokens and components are in /site.css */
|
||||
.lhead{padding:var(--s-6) 0 var(--s-5)}
|
||||
.lhead .wrap{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:var(--s-3) var(--s-5)}
|
||||
.lhead h1{font-size:var(--t-h1);margin:var(--s-2) 0 0}
|
||||
.lhead{padding-bottom:var(--s-4)}
|
||||
.lhead p{margin:var(--s-3) 0 0;color:var(--ink-2);max-width:60ch}
|
||||
.bleed{width:100%;padding:0 var(--gutter)}
|
||||
.bleed canvas{width:100%;height:min(54vh,520px);min-height:300px;display:block;border-radius:var(--r-card);background:var(--row);border:1px solid var(--line)}
|
||||
.caption{margin:var(--s-3) auto 0;font:400 13px/1.5 var(--f-mono);color:var(--bone);min-height:1.5em;max-width:var(--max);padding:0 var(--gutter)}
|
||||
.caption.muted{color:var(--ash)}
|
||||
.strip{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:var(--s-3);margin-top:var(--s-6)}
|
||||
@media (max-width:1000px){.strip{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
||||
@media (max-width:600px){.strip{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.cell{background:var(--graphite);border:1px solid var(--line);border-radius:var(--r-row);padding:var(--s-4);min-width:0}
|
||||
.cell .k{font:500 var(--t-xs)/1.4 var(--f-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ash)}
|
||||
.cell .v{font:700 clamp(20px,2.2vw,28px)/1.1 var(--f-display);color:var(--bone);margin-top:6px;overflow-wrap:anywhere}
|
||||
.cell .v.state{font:500 var(--t-base)/1.3 var(--f-mono);color:var(--ash)}
|
||||
.cell .s{font:400 var(--t-sm)/1.4 var(--f-mono);color:var(--ash);margin-top:4px}
|
||||
.fin{display:flex;align-items:center;gap:var(--s-3);margin:var(--s-5) 0 0;padding:var(--s-4) var(--s-5);border:1px solid var(--line);border-radius:var(--r-row);background:var(--row);color:var(--ink-2);font-size:var(--t-md)}
|
||||
.fin b{color:var(--bone)}
|
||||
.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4);margin-top:var(--s-5)}
|
||||
@media (max-width:1000px){.three{grid-template-columns:1fr}}
|
||||
.three h2{font-size:var(--t-h3);margin-bottom:var(--s-3)}
|
||||
.lanes{display:flex;flex-direction:column}
|
||||
.lane{display:grid;grid-template-columns:84px minmax(0,1fr) 72px;gap:12px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line);font:400 13px/1.4 var(--f-mono);color:var(--ink-2)}
|
||||
.lane:last-child{border-bottom:0}
|
||||
.lane .id{color:var(--molten-text)}.lane .n{text-align:right;white-space:nowrap}
|
||||
.bar{height:6px;border-radius:3px;background:var(--line);overflow:hidden;min-width:40px}
|
||||
.bar i{display:block;height:100%;background:var(--ember)}
|
||||
.kv2{display:grid;grid-template-columns:1fr auto;gap:6px 12px;font:400 13px/1.5 var(--f-mono);color:var(--ink-2)}
|
||||
.kv2 b{color:var(--bone);font-weight:500;text-align:right;white-space:nowrap}
|
||||
.blocks{display:flex;flex-direction:column;font:400 13px/1.4 var(--f-mono);color:var(--ink-2)}
|
||||
.blocks div{display:grid;grid-template-columns:minmax(72px,auto) minmax(0,1fr) auto;gap:10px;padding:7px 0;border-bottom:1px solid var(--line);align-items:baseline}
|
||||
.blocks div>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.blocks div:last-child{border-bottom:0}
|
||||
.blocks .w{color:var(--ash);white-space:nowrap}.blocks .w.hot{color:var(--molten-text)}.blocks .num{color:var(--bone)}
|
||||
.blocks div.new{animation:land .9s ease-out}
|
||||
@keyframes land{0%{background:var(--ember-12)}100%{background:transparent}}
|
||||
@media (prefers-reduced-motion:reduce){.blocks div.new{animation:none}}
|
||||
.lc{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:var(--s-2) var(--s-4);margin-top:var(--s-5);padding:var(--s-3) var(--s-4);border:1px solid var(--line);border-radius:var(--r-row);background:var(--row);font-size:var(--t-base);color:var(--ink-2)}
|
||||
.lc .tick{width:28px;height:28px;border-radius:8px;background:var(--ember);display:grid;place-items:center;flex:none;color:var(--ember-ink)}
|
||||
.lc [data-lc]{color:var(--bone)}
|
||||
@media (max-width:700px){.lc{grid-template-columns:auto minmax(0,1fr)}.lc .state{grid-column:2}}
|
||||
/* live page block (7 Oct 2026, site-ui-4: the observatory composition, every number read from the observer); tokens and components are in /site.css */
|
||||
.obs{--pr:8px;--hair:1px solid var(--line)}
|
||||
.obs-head{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:var(--s-4) var(--s-5);padding:var(--s-6) 0 var(--s-5)}
|
||||
.obs-head .eyebrow{display:flex;align-items:center;gap:8px;margin-bottom:var(--s-3)}
|
||||
.obs-head .eyebrow i{width:7px;height:7px;background:var(--ember);transform:rotate(45deg);display:inline-block}
|
||||
.obs-head h1{font-size:clamp(34px,4.6vw,56px);font-weight:700;letter-spacing:-.02em;margin:0}
|
||||
.obs-head h1 b{color:var(--ember-text)}
|
||||
.obs-head p{margin:var(--s-3) 0 0;color:var(--ink-2);max-width:60ch}
|
||||
.obs-head .side{display:flex;flex-direction:column;align-items:flex-end;gap:var(--s-2)}
|
||||
.obs-head .side .state{font-size:var(--t-xs)}
|
||||
.tiles6{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));border:var(--hair);border-radius:var(--pr);background:var(--graphite);overflow:hidden}
|
||||
.tiles6>div{padding:18px 19px 19px;border-right:var(--hair);min-width:0}
|
||||
.tiles6>div:last-child{border-right:0}
|
||||
.tiles6 .k{display:flex;justify-content:space-between;align-items:center;font:400 13px/1.4 var(--f-sans);color:var(--ink-2)}
|
||||
.tiles6 .k i{color:var(--ash);font:400 12px/1 var(--f-mono)}
|
||||
.tiles6 .v{font:500 clamp(22px,2vw,30px)/1.15 var(--f-sans);color:var(--bone);margin:14px 0 10px;letter-spacing:-.01em;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
|
||||
.tiles6 .v.word{font:500 15px/1.3 var(--f-mono);color:var(--ash)}
|
||||
.tiles6 .d{font:400 11px/1.4 var(--f-mono);color:var(--ash)}
|
||||
@media (max-width:1100px){.tiles6{grid-template-columns:repeat(3,minmax(0,1fr))}.tiles6>div:nth-child(3){border-right:0}.tiles6>div:nth-child(-n+3){border-bottom:var(--hair)}}
|
||||
@media (max-width:600px){.tiles6{grid-template-columns:repeat(2,minmax(0,1fr))}.tiles6>div{border-right:var(--hair)!important;border-bottom:var(--hair)}.tiles6>div:nth-child(2n){border-right:0!important}.tiles6>div:nth-last-child(-n+2){border-bottom:0}}
|
||||
.panel{border:var(--hair);border-radius:var(--pr);background:var(--graphite);margin-top:var(--s-5);overflow:hidden}
|
||||
.toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:15px 21px;border-bottom:var(--hair);background:var(--row)}
|
||||
.toolbar .title{display:flex;align-items:center;gap:10px;font:500 var(--t-md)/1.3 var(--f-sans);color:var(--bone)}
|
||||
.toolbar .title i{width:14px;height:14px;border:1.5px solid var(--ember);border-radius:50%;display:inline-block;position:relative}
|
||||
.toolbar .title i::after{content:"";position:absolute;inset:4px;background:var(--ember);border-radius:50%}
|
||||
.chip{font:400 11px/1.2 var(--f-mono);color:var(--ash);border:var(--hair);border-radius:4px;padding:5px 8px;white-space:nowrap}
|
||||
.toolbar .right{margin-left:auto;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.tb{background:transparent;border:1px solid transparent;color:var(--ash);font:400 13px/1.2 var(--f-sans);padding:7px 10px;border-radius:6px;cursor:pointer}
|
||||
.tb:hover{color:var(--bone)}
|
||||
.tb[aria-pressed="true"]{color:var(--bone);border-color:var(--line-2);background:var(--graphite)}
|
||||
.tb.on{color:var(--ember-text);border-color:var(--ember-40)}
|
||||
.sep{width:1px;height:18px;background:var(--line)}
|
||||
.graph{display:grid;grid-template-columns:minmax(0,1fr) 250px}
|
||||
@media (max-width:1000px){.graph{grid-template-columns:1fr}}
|
||||
.gcol{padding:18px 21px 14px;min-width:0;border-right:var(--hair)}
|
||||
@media (max-width:1000px){.gcol{border-right:0;border-bottom:var(--hair)}}
|
||||
.gcol .state{margin-bottom:10px;font-size:var(--t-xs)}
|
||||
.scene{position:relative}
|
||||
#dag{display:block;width:100%;height:clamp(280px,38vw,420px);border-radius:var(--r-row);background:var(--row);border:1px solid var(--line);touch-action:pan-y}
|
||||
.tip{position:absolute;left:0;top:0;pointer-events:none;background:var(--graphite);border:1px solid var(--line-2);border-radius:var(--r-row);padding:10px 12px;font:400 12px/1.5 var(--f-mono);color:var(--ink-2);white-space:nowrap;z-index:5}
|
||||
#dag{display:block;width:100%;height:clamp(300px,36vw,440px);border-radius:6px;background:var(--row);touch-action:pan-y}
|
||||
.tip{position:absolute;left:0;top:0;pointer-events:none;background:var(--graphite);border:1px solid var(--line-2);border-radius:6px;padding:10px 12px;font:400 12px/1.5 var(--f-mono);color:var(--ink-2);white-space:nowrap;z-index:5}
|
||||
.tip[hidden]{display:none}
|
||||
.dl-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--s-3);margin-top:var(--s-6);padding-top:var(--s-5);border-top:1px solid var(--line)}
|
||||
.gfoot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-top:12px;font:400 11px/1.4 var(--f-mono);color:var(--ash)}
|
||||
.zoom{display:inline-flex;align-items:center;border:var(--hair);border-radius:6px;overflow:hidden}
|
||||
.zoom button{background:transparent;border:0;color:var(--ink-2);font:400 13px/1 var(--f-mono);padding:8px 12px;cursor:pointer}
|
||||
.zoom button:hover{background:var(--hover)}
|
||||
.zoom span{font:400 11px/1 var(--f-mono);color:var(--ash);padding:0 6px;min-width:44px;text-align:center}
|
||||
.icol{padding:18px 21px;min-width:0;font-size:13px}
|
||||
.icol .ih{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:14px}
|
||||
.icol .ih .eyebrow{margin:0}
|
||||
.icol .num{font:500 22px/1.2 var(--f-sans);color:var(--bone);margin:6px 0 4px;overflow-wrap:anywhere}
|
||||
.icol .hash{font:400 11px/1.5 var(--f-mono);color:var(--ash);overflow-wrap:anywhere;margin-bottom:14px}
|
||||
.insp{display:grid;grid-template-columns:auto minmax(0,1fr);gap:0 10px;margin:0;border-top:var(--hair);padding-top:8px}
|
||||
.insp dt{font:400 11px/1.3 var(--f-mono);color:var(--ash);padding:6px 0}
|
||||
.insp dd{margin:0;font:400 11px/1.3 var(--f-mono);color:var(--bone);text-align:right;padding:6px 0;overflow-wrap:anywhere;min-width:0}
|
||||
.insp dd.hot{color:var(--molten-text)}
|
||||
.icol .note{font-size:12px;margin-top:14px;padding-top:12px;border-top:var(--hair)}
|
||||
.insp-empty{color:var(--ash);font-size:13px;line-height:1.5;padding:6px 0 2px}
|
||||
.mstats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border-bottom:var(--hair)}
|
||||
.mstats>div{padding:16px 21px 14px;border-right:var(--hair);min-width:0}
|
||||
.mstats>div:last-child{border-right:0}
|
||||
.mstats .mk{font:400 13px/1.4 var(--f-sans);color:var(--ink-2)}
|
||||
.mstats .mv{display:flex;align-items:baseline;gap:8px;margin:8px 0 6px}
|
||||
.mstats .mv b{font:500 26px/1.1 var(--f-sans);color:var(--bone);letter-spacing:-.01em}
|
||||
.mstats .mv b.own{color:var(--molten-text)}.mstats .mv b.dim{color:var(--ash)}
|
||||
.mstats .mv small{font:400 11px/1.4 var(--f-mono);color:var(--ash)}
|
||||
.mstats .mf{font:400 11px/1.4 var(--f-mono);color:var(--ash);overflow-wrap:anywhere}
|
||||
@media (max-width:900px){.mstats{grid-template-columns:repeat(2,minmax(0,1fr))}.mstats>div:nth-child(2){border-right:0}.mstats>div:nth-child(-n+2){border-bottom:var(--hair)}}
|
||||
.tabs{display:inline-flex;border:var(--hair);border-radius:6px;padding:3px;gap:2px;background:var(--graphite)}
|
||||
.tab{background:transparent;border:0;color:var(--ash);font:400 12px/1.2 var(--f-sans);padding:6px 10px;border-radius:4px;cursor:pointer}
|
||||
.tab.on{background:var(--row);color:var(--bone)}
|
||||
.ititle{font:500 22px/1.2 var(--f-sans);color:var(--bone);margin:6px 0 4px;letter-spacing:-.01em}
|
||||
.proof-scene{height:200px;border:var(--hair);border-radius:6px;background:var(--row);margin:12px 0;overflow:hidden}
|
||||
.proof-scene canvas{display:block;width:100%;height:100%}
|
||||
.proof-head{display:flex;justify-content:space-between;align-items:baseline;font:500 13px/1.3 var(--f-sans);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)}
|
||||
.shards{list-style:none;margin:0 0 12px;padding:0;font:400 11px/1.6 var(--f-mono)}
|
||||
.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);vertical-align:middle}
|
||||
.shards li.proving,.shards li.proving::before{color:var(--ember-text)}.shards li.verified,.shards li.verified::before{color:var(--bone)}.shards li.paid,.shards li.paid::before{color:var(--molten-text)}
|
||||
.lgi{width:10px;height:10px;border-radius:3px;display:inline-block;flex:none;position:relative}
|
||||
.lgi.tick::after{content:"\2713";position:absolute;left:0;top:-4px;font-size:11px;color:var(--bone)}
|
||||
.motion{margin-top:var(--s-5);border:var(--hair);border-radius:7px;background:var(--graphite);padding:22px 21px}
|
||||
.motion h2{font:500 var(--t-h3)/1.3 var(--f-sans);margin:0 0 14px;color:var(--bone)}
|
||||
.beats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4)}
|
||||
@media (max-width:900px){.beats{grid-template-columns:1fr}}
|
||||
.beats .num{font:400 10px/1 var(--f-mono);color:var(--ember-text)}
|
||||
.beats h3{font:500 14px/1.4 var(--f-sans);margin:6px 0 4px;color:var(--bone)}
|
||||
.beats p{margin:0;font-size:13px;color:var(--ink-2);line-height:1.5}
|
||||
.legend-row{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 18px;padding:12px 21px;border-top:var(--hair);font:400 11px/1.4 var(--f-mono);color:var(--ash)}
|
||||
.legend-row .lg{display:flex;flex-wrap:wrap;gap:8px 16px}
|
||||
.legend-row .lg span{display:inline-flex;align-items:center;gap:7px;color:var(--ink-2);font-family:var(--f-sans);font-size:12px}
|
||||
.sw{width:9px;height:9px;transform:rotate(45deg);display:inline-block;flex:none}
|
||||
.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4);margin-top:var(--s-4)}
|
||||
@media (max-width:1000px){.three{grid-template-columns:1fr}}
|
||||
.dp{border:var(--hair);border-radius:7px;background:var(--graphite);padding:20px 21px 16px;min-width:0}
|
||||
.dp .ph{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:14px}
|
||||
.dp h2{font:500 var(--t-md)/1.3 var(--f-sans);margin:0;color:var(--bone)}
|
||||
.dp .tag{font:400 10px/1.2 var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
|
||||
.big{display:flex;align-items:baseline;gap:10px;margin-bottom:8px}
|
||||
.big b{font:500 30px/1 var(--f-sans);color:var(--bone)}
|
||||
.big span{font:400 12px/1.4 var(--f-sans);color:var(--ash)}
|
||||
.bars{display:flex;align-items:flex-end;gap:3px;height:80px;margin-top:10px}
|
||||
.bars i{flex:1 1 0;background:linear-gradient(to top,var(--ember-40),var(--molten));border-radius:2px 2px 0 0;min-height:2px;opacity:.9}
|
||||
.bars i.now{background:var(--molten)}
|
||||
.axis{display:flex;justify-content:space-between;font:400 10px/1.4 var(--f-mono);color:var(--ash);margin-top:8px}
|
||||
.ids{display:flex;flex-direction:column;gap:12px}
|
||||
.id-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 10px;font:400 12px/1.4 var(--f-mono);color:var(--ink-2);align-items:center}
|
||||
.id-row .name{display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.id-row .name .sw{background:var(--line-2)}
|
||||
.id-row.mine .name{color:var(--ember-text)}
|
||||
.id-row.mine .name .sw{background:var(--ember)}
|
||||
.id-row .pct{color:var(--bone);white-space:nowrap}
|
||||
.id-row .bar{grid-column:1/-1;height:3px;background:var(--line);border-radius:2px;overflow:hidden}
|
||||
.id-row .bar i{display:block;height:100%;background:var(--ember);opacity:.85}
|
||||
.id-row.mine .bar i{background:var(--molten)}
|
||||
.evs{display:flex;flex-direction:column;max-height:330px;overflow:auto}
|
||||
.evs div{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:baseline;padding:9px 0;border-bottom:var(--hair);font:400 12px/1.4 var(--f-mono);color:var(--ink-2)}
|
||||
.evs div:last-child{border-bottom:0}
|
||||
.evs .dot{width:6px;height:6px;background:var(--molten);border-radius:50%;animation:none}
|
||||
.evs .dot.lock{background:var(--ember)}.evs .dot.quiet{background:var(--ash)}
|
||||
.evs .what{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.evs .what b{font-weight:500;color:var(--bone)}
|
||||
.evs .t{color:var(--ash);font-size:11px;white-space:nowrap}
|
||||
.evs div.new{animation:land .9s ease-out}
|
||||
@keyframes land{0%{background:var(--ember-12)}100%{background:transparent}}
|
||||
@media (prefers-reduced-motion:reduce){.evs div.new{animation:none}}
|
||||
.tablebar{margin-top:var(--s-4);border:var(--hair);border-radius:7px;background:var(--graphite)}
|
||||
.tablebar summary{padding:16px 21px;font:500 var(--t-md)/1.3 var(--f-sans);color:var(--bone)}
|
||||
.tablebar summary::after{content:"+";font:400 14px/1 var(--f-mono);color:var(--ash)}
|
||||
.tablebar[open] summary::after{content:"\2212"}
|
||||
.tablebar summary small{font:400 11px/1.4 var(--f-mono);color:var(--ash);margin-left:auto;margin-right:12px}
|
||||
.tablebar .tbl{border:0;border-top:var(--hair);border-radius:0}
|
||||
.tablebar table{font-size:12px;font-family:var(--f-mono)}
|
||||
.tablebar td,.tablebar th{padding:8px 21px}
|
||||
.tablebar tr[data-hash]{cursor:pointer}
|
||||
.tablebar tr[data-hash]:hover td{background:var(--hover)}
|
||||
.obs-foot{display:flex;flex-wrap:wrap;justify-content:space-between;gap:10px;margin-top:var(--s-5);font:400 10px/1.5 var(--f-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ash)}
|
||||
.linkish{background:none;border:0;padding:0;color:var(--ember-text);font:inherit;cursor:pointer}
|
||||
.linkish:hover{text-decoration:underline}
|
||||
details.tablebar{border-top:var(--hair);border-bottom:var(--hair)}
|
||||
details.tablebar summary{display:flex;align-items:center}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -151,90 +246,131 @@
|
|||
</script>
|
||||
<!-- nav:end -->
|
||||
|
||||
<main id="main">
|
||||
<header class="lhead" id="top">
|
||||
<div class="wrap">
|
||||
<main id="main" class="wrap obs">
|
||||
<header class="obs-head" id="top">
|
||||
<div>
|
||||
<div class="state" id="live-state"><span class="dot off"></span>connecting to the devnet observer</div>
|
||||
<h1>The chain, as it happens.</h1>
|
||||
<div class="eyebrow"><i></i>Network observatory</div>
|
||||
<h1>Live devnet<b>.</b></h1>
|
||||
<p>Every block has a story. Follow the connections.</p>
|
||||
</div>
|
||||
<a href="/miner#get" class="btn primary">Download the miner</a>
|
||||
<div class="side">
|
||||
<div class="cta"><a href="/miner#get" class="btn primary">Download the miner</a><a href="/explorer" class="btn">Open the explorer</a></div>
|
||||
<div class="state" id="live-state"><span class="dot off"></span>connecting to the observer</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tiles6" aria-live="polite">
|
||||
<div><div class="k">Blocks / second <i>↗</i></div><div class="v tick" id="s-bps">pending</div><div class="d">counted over the node’s last 60 s; the target is 1</div></div>
|
||||
<div><div class="k">Blue score <i>#</i></div><div class="v tick" id="s-blue">pending</div><div class="d">the newest block’s count of blue ancestors</div></div>
|
||||
<div><div class="k">Network hash rate <i>∿</i></div><div class="v tick" id="s-hash">pending</div><div class="d">the node’s estimate from the last 1,000 blocks’ difficulty</div></div>
|
||||
<div><div class="k">Active vote keys <i>◇</i></div><div class="v tick" id="s-keys">pending</div><div class="d">keys with a block in 10 min; a card runs several</div></div>
|
||||
<div><div class="k">Connected peers <i>⇄</i></div><div class="v tick" id="s-peers">pending</div><div class="d">peers of the observer’s node</div></div>
|
||||
<div><div class="k">Locked checkpoint <i>●</i></div><div class="v tick" id="s-lock">pending</div><div class="d" id="s-lock-d">signed by two thirds of all 30-day weight</div></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="bleed">
|
||||
<canvas id="steps" aria-label="The live devnet, step by step: every square is a real block moving left as time passes; its outline, quarter cells, fill and ring show mined, shards being proven, proven and locked checkpoint"></canvas>
|
||||
</div>
|
||||
<p class="caption mono" id="steps-caption" aria-live="polite">Connecting to the devnet observer.</p>
|
||||
|
||||
<section class="sec" style="padding-top:0">
|
||||
<div class="wrap">
|
||||
<div class="strip" aria-live="polite">
|
||||
<div class="cell"><div class="k">Hash rate</div><div class="v tick" id="s-hash">pending</div><div class="s">network estimate</div></div>
|
||||
<div class="cell"><div class="k">Blocks, 10 min</div><div class="v tick" id="s-b10">pending</div><div class="s">target 1 a second</div></div>
|
||||
<div class="cell"><div class="k">Latest block</div><div class="v tick" id="s-last">pending</div><div class="s" id="s-last-s">pending</div></div>
|
||||
<div class="cell"><div class="k">Shards proven, paid</div><div class="v tick" id="s-paid">pending</div><div class="s" id="s-paid-s">pending</div></div>
|
||||
<div class="cell"><div class="k">Vote keys, 10 min</div><div class="v tick" id="s-keys">pending</div><div class="s">a card runs several</div></div>
|
||||
<div class="cell"><div class="k">Last lock</div><div class="v tick" id="s-lock">pending</div><div class="s" id="s-lock-s">pending</div></div>
|
||||
<section class="panel" aria-label="The block graph">
|
||||
<div class="mstats">
|
||||
<div><div class="mk">Blocks in window</div><div class="mv"><b id="m-blocks">0</b><small id="m-win">last 90 s</small></div><div class="mf" id="m-keys">waiting for the observer</div></div>
|
||||
<div><div class="mk">Your blocks</div><div class="mv"><b id="m-mine" class="own">0</b><small>in this window</small></div><div class="mf">Key <span class="mono" id="m-key">not set</span> · <button type="button" class="linkish" id="mine-btn">Mark my key</button></div></div>
|
||||
<div><div class="mk">Proven blocks</div><div class="mv"><b id="m-proven">0</b><small id="m-proven-pct">no blocks</small></div><div class="mf">observer-reported proof status</div></div>
|
||||
<div><div class="mk">Latest checkpoint</div><div class="mv"><b id="m-cp">None</b><small id="m-cp-w"></small></div><div class="mf" id="m-cp-note">no checkpoint in the reply</div></div>
|
||||
</div>
|
||||
<p class="fin" id="fin"><span class="dot off"></span><span>Reading the finality rule.</span></p>
|
||||
<p class="note">One node is read every 2 s; nothing here is a replay. Every square in the scene is a real block: its outline, fill and ring are its state. Fees since DAA 210,000: <a href="/litepaper#economics">the adopted fee table</a>.</p>
|
||||
|
||||
<div class="three">
|
||||
<div class="card">
|
||||
<div class="viz-head"><h2>Miners</h2><div class="eyebrow" id="miners-count">last 10 min</div></div>
|
||||
<div class="lanes" id="miners"><div class="empty">No blocks in the last 10 minutes.</div></div>
|
||||
<p class="note" id="miners-more">Every vote key with a block in the last 10 minutes; a card runs several.</p>
|
||||
<div class="toolbar">
|
||||
<div class="title"><i></i>The block graph</div>
|
||||
<span class="chip" id="c-count">0 blocks</span>
|
||||
<div class="tabs" role="tablist" aria-label="Which blocks">
|
||||
<button type="button" class="tab on" data-filter="all" aria-pressed="true">All blocks</button>
|
||||
<button type="button" class="tab" data-filter="chain" aria-pressed="false">Selected chain</button>
|
||||
<button type="button" class="tab" data-filter="mine" aria-pressed="false">Your blocks</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="viz-head"><h2>Provers paid</h2><div class="eyebrow" id="prov-state">pending</div></div>
|
||||
<div class="kv2" id="prov">
|
||||
<span>shards proven and paid, all time</span><b id="p-total">pending</b>
|
||||
<span>paid in the last 10 min</span><b id="p-10">pending</b>
|
||||
<span>blocks fully proven, 10 min</span><b id="p-full">pending</b>
|
||||
<span>proofs land behind the tip</span><b id="p-lag">pending</b>
|
||||
<div class="right">
|
||||
<span class="chip" id="lane-caption">keys</span>
|
||||
<span class="sep"></span>
|
||||
<button type="button" class="tb" id="dag-pause" aria-pressed="false">Pause</button>
|
||||
<button type="button" class="tb on" id="dag-follow">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="graph">
|
||||
<div class="gcol">
|
||||
<div class="state" id="dag-state"><span class="dot off"></span>connecting to the observer</div>
|
||||
<div class="scene"><canvas id="dag" aria-label="The devnet's block graph, live: time-aligned miner lanes, every parent connection, the selected chain as one path, checkpoint bands, your blocks framed"></canvas><div class="tip" id="dag-tip" hidden></div><div class="chip-zoom" id="dag-chip" hidden>scroll to zoom · Esc to release</div></div>
|
||||
<div class="gfoot">
|
||||
<span>Click to inspect · Drag to explore</span>
|
||||
<span class="zoom"><button type="button" id="z-out" aria-label="Wider window">−</button><span id="z-pct">60 s</span><button type="button" id="z-in" aria-label="Narrower window">+</button></span>
|
||||
</div>
|
||||
<p class="note">Every chain block is split into shards; a card proves one, posts it, and is paid on the chain.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="viz-head"><h2>Recent blocks</h2><div class="eyebrow">the last eight</div></div>
|
||||
<div class="blocks" id="blocks"><div class="empty">No blocks yet.</div></div>
|
||||
<p class="note">Number, miner, state. A number arrives once the block is planned onto the chain.</p>
|
||||
</div>
|
||||
<aside class="icol" id="inspector" aria-live="polite">
|
||||
<div class="ih"><div class="eyebrow">Block inspector</div><span class="chip" id="i-kind">select a block</span></div>
|
||||
<h3 class="ititle" id="i-title">Select a block</h3>
|
||||
<div class="hash" id="i-hash">Click the graph, or a row of the table below.</div>
|
||||
<div class="proof-scene"><canvas id="proof" aria-label="Select a block to see its shards"></canvas></div>
|
||||
<div class="proof-head"><span>Proof shards</span><span id="i-shard-count" class="mono">not set</span></div>
|
||||
<div class="track" id="i-track"></div>
|
||||
<ul class="shards" id="i-shards"><li class="muted">No block selected.</li></ul>
|
||||
<dl class="insp">
|
||||
<dt>Inclusion</dt><dd id="i-incl">not set</dd>
|
||||
<dt>Miner key</dt><dd id="i-miner">not set</dd>
|
||||
<dt>Blue score</dt><dd id="i-blue">not set</dd>
|
||||
<dt>DAA score</dt><dd id="i-daa">not set</dd>
|
||||
<dt>Parent links</dt><dd id="i-parents">not set</dd>
|
||||
<dt>Header time</dt><dd id="i-time">not set</dd>
|
||||
<dt>Checkpoint</dt><dd id="i-cp">not set</dd>
|
||||
<dt>Finality</dt><dd id="i-lock">not set</dd>
|
||||
</dl>
|
||||
<p class="note">Proof, chain selection and finality are separate states. None is inferred from a block’s age; a lock is drawn only when the observer reports one.</p>
|
||||
<p style="margin:10px 0 0"><button type="button" class="linkish" id="insp-close" hidden>Clear</button></p>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="lc" id="light-client">
|
||||
<span class="tick"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"></path></svg></span>
|
||||
<span>This tab checks the chain itself: checkpoint <span data-lc="checkpoint">checking</span>, <span data-lc="tab">checking</span>. <span data-lc="line" class="muted"></span></span>
|
||||
<span class="state" data-lc="badge"><span class="dot off"></span><span data-lc="badge-text">PREVIEW</span></span>
|
||||
<span hidden data-lc="proof"></span><span hidden data-lc="system"></span>
|
||||
<script type="module" src="/verify/verify.js"></script>
|
||||
</div>
|
||||
|
||||
<div class="card viz" style="margin-top:var(--s-5)" aria-label="Block DAG, finality and proving, live">
|
||||
<div class="viz-head">
|
||||
<div class="state" id="dag-state"><span class="dot off"></span>the DAG</div>
|
||||
<div class="viz-stats mono"><span>on screen <b id="c-screen">0</b></span><span>chain <b id="c-chain">0</b></span><span><span id="c-window">120 s</span> on screen</span><button type="button" class="btn small" id="dag-pause">Pause</button></div>
|
||||
<div class="legend-row">
|
||||
<div class="lg">
|
||||
<span><i class="lgi" style="border:1.5px solid var(--included)"></i>Included</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--excluded)"></i>Excluded</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--ember);background:var(--ember-12)"></i>Selected chain</span>
|
||||
<span><i class="lgi" style="border:1.5px dashed var(--molten)"></i>Your block</span>
|
||||
<span><i class="lgi tick"></i>Proven</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--ember);border-radius:50%"></i>Locked checkpoint</span>
|
||||
</div>
|
||||
<div class="scene"><canvas id="dag" aria-label="The devnet's block DAG, live: blocks by time and miner lane, parent edges, the selected chain, checkpoint bands with their lock weight, proofs filling blocks as segments are paid"></canvas><div class="tip" id="dag-tip" hidden></div><div class="chip-zoom" id="dag-chip" hidden>scroll to zoom · Esc to release</div></div>
|
||||
<div class="legend">
|
||||
<span><i class="sw" style="border:2px solid var(--line-2)"></i>pending</span>
|
||||
<span><i class="sw" style="border:2px solid var(--bone)"></i>included</span>
|
||||
<span><i class="sw" style="border:2px solid var(--ember)"></i>selected chain</span>
|
||||
<span><i class="sw" style="border:2px solid var(--line-2);opacity:.45"></i>excluded</span>
|
||||
<span><i class="sw" style="background:var(--ember)"></i>proven</span>
|
||||
<span><i class="sw" style="border:3px solid var(--ember);border-radius:50%;background:transparent"></i>locked checkpoint</span>
|
||||
<span><i class="sw" style="width:0;height:12px;border-left:2px solid var(--ember);border-radius:0"></i>finality band, locked, with its weight</span>
|
||||
</div>
|
||||
<p class="note" style="margin-top:var(--s-3)">One lane per miner, the busiest first; the rest share the last lane. Hover or tap a block for its hash, miner, blue score, parents and proof state. Ctrl or cmd and scroll, a pinch, or a click into the scene zooms the window. <a href="/explorer">The explorer</a> has every block.</p>
|
||||
<span id="blue-range">blue score pending</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="dl-row">
|
||||
<p class="yourcard" data-yourcard style="margin:0;flex:1 1 420px">Your card: pending</p>
|
||||
<a href="/miner#get" class="btn primary">Download the miner</a>
|
||||
<section class="motion">
|
||||
<h2>Motion, with meaning.</h2>
|
||||
<div class="beats">
|
||||
<div><span class="num">01</span><h3>A block arrives.</h3><p>A brief signal traces its real parent edges. Your blocks take a molten frame.</p></div>
|
||||
<div><span class="num">02</span><h3>The proof takes shape.</h3><p>Shard tiles follow the planned, proving, verified and paid states the observer reports.</p></div>
|
||||
<div><span class="num">03</span><h3>A checkpoint locks.</h3><p>A pending marker becomes a solid band only when the observer reports it locked.</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="three">
|
||||
<div class="dp">
|
||||
<div class="ph"><h2>Block activity</h2><span class="tag">header times / window</span></div>
|
||||
<div class="big"><b id="a-count">0</b><span>blocks in this view</span></div>
|
||||
<div class="bars" id="a-bars" aria-label="Blocks per minute over the last hour, newest on the right"></div>
|
||||
<div class="axis"><span>one bar a minute, the last 30 min</span><span>now</span></div>
|
||||
<p class="note" id="a-note" style="margin-top:12px;font-size:12px">Counted by the observer as blocks arrive.</p>
|
||||
</div>
|
||||
<div class="dp">
|
||||
<div class="ph"><h2>Mining identities</h2><span class="tag">displayed window</span></div>
|
||||
<div class="ids" id="miners"><div class="empty">No blocks in the window.</div></div>
|
||||
<p class="note" style="margin-top:14px;font-size:12px" id="miners-more">Vote-key share of the displayed blocks. Several keys may belong to one GPU.</p>
|
||||
</div>
|
||||
<div class="dp">
|
||||
<div class="ph"><h2>Event stream</h2><span class="tag" id="ev-count">live</span></div>
|
||||
<div class="evs" id="events"><div class="empty">Waiting for the first read.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details class="tablebar" id="table">
|
||||
<summary>Explore blocks as a table <small>keyboard-accessible view</small></summary>
|
||||
<div class="tbl"><table>
|
||||
<thead><tr><th>Blue score</th><th>Hash</th><th>Vote key</th><th>Parents</th><th>State</th><th>Chain block</th></tr></thead>
|
||||
<tbody id="tbody"><tr><td colspan="6">Waiting for the first read.</td></tr></tbody>
|
||||
</table></div>
|
||||
</details>
|
||||
|
||||
<div class="obs-foot"><span>Igneum / observatory</span><span>One node read every 2 s. Nothing here is a replay.</span></div>
|
||||
</main>
|
||||
|
||||
<!-- footer:start -->
|
||||
|
|
@ -298,64 +434,99 @@
|
|||
</script>
|
||||
<!-- footer:end -->
|
||||
|
||||
<script src="/scenes/feed.js"></script>
|
||||
<script src="/live-steps.js"></script>
|
||||
<script src="/live-dag.js" defer></script>
|
||||
<script src="/yourcard.js" defer></script>
|
||||
<script src="/proof-core.js" defer></script>
|
||||
<script>
|
||||
(function(){
|
||||
function $(id){return document.getElementById(id);}
|
||||
var fmtN=function(v){return v===null||v===undefined?'pending':Number(v).toLocaleString('en-GB');};
|
||||
function fmtHash(h){if(h===null||h===undefined||!(h>0))return 'pending';return h>=1e9?(h/1e9).toFixed(1)+' GH/s':(h/1e6).toFixed(1)+' MH/s';}
|
||||
function rel(ms){var s=Math.max(0,Math.round(ms/1000));if(s<60)return s+' s ago';var m=Math.floor(s/60);if(m<60)return m+' min ago';var h=Math.floor(m/60);if(h<24)return h+' h ago';return Math.floor(h/24)+' d ago';}
|
||||
function hms(ms){var d=new Date(ms);return String(d.getUTCHours()).padStart(2,'0')+':'+String(d.getUTCMinutes()).padStart(2,'0')+':'+String(d.getUTCSeconds()).padStart(2,'0');}
|
||||
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||||
// a counter that changed glows for half a second: motion only from a real change
|
||||
// a number that changed glows for a frame: motion only from a real change
|
||||
function set(id,text){var el=$(id);if(!el||el.textContent===text)return;el.textContent=text;el.classList.add('flash');setTimeout(function(){el.classList.remove('flash');},60);}
|
||||
var cap=$('steps-caption'),lastTs=null,seen={};
|
||||
function onState(s,r){var el=$('live-state');el.className='state '+(s==='live'?'live':s==='failed'?'bad':'');el.innerHTML='<span class="dot '+(s==='live'?'live':s==='failed'?'bad':'off')+'"></span>'+(s==='live'?'live devnet':s==='connecting'?'connecting to the devnet observer':esc(r||s));
|
||||
if(s==='live')return;cap.classList.add('muted');cap.textContent=s==='connecting'?'Connecting to the devnet observer.':s==='stale'?(r.charAt(0).toUpperCase()+r.slice(1))+'. The scene holds the last stored blocks.':'Live feed unavailable. The scene holds its last blocks.';}
|
||||
function onData(d){var s=d.state,pv=d.proving,fn=d.finality,now=new Date(d.now).getTime();
|
||||
set('s-hash',fmtHash(s.hashes_per_second_estimate));
|
||||
var bpm=s.blocks_per_minute||[],b10=bpm.slice(-10).reduce(function(a,p){return a+(p[1]||0);},0);set('s-b10',fmtN(b10));
|
||||
var newest=null;(d.blocks||[]).forEach(function(b){if(!newest||b.ts>newest.ts)newest=b;});
|
||||
if(newest){lastTs=newest.ts;$('s-last-s').textContent=newest.number?'block '+fmtN(newest.number)+(newest.chain?', on the selected chain':''):'block '+newest.hash+', not yet planned';}
|
||||
if(pv&&pv.supported&&pv.active){set('s-paid',fmtN(pv.paid_shards_total));$('s-paid-s').textContent=fmtN(pv.shards_paid_10m||0)+' in the last 10 min';$('prov-state').textContent='live';
|
||||
$('p-total').textContent=fmtN(pv.paid_shards_total);$('p-10').textContent=fmtN(pv.shards_paid_10m||0);$('p-full').textContent=fmtN(pv.blocks_fully_proven_10m||0)+' of '+fmtN(pv.blocks_10m||0);$('p-lag').textContent=pv.median_proof_lag_s?'a median '+Math.round(pv.median_proof_lag_s)+' s':'pending';}
|
||||
else{var w=pv&&pv.supported?'proving not active':'no proving layer';$('s-paid').textContent=w;$('s-paid').className='v state';$('s-paid-s').textContent='';$('prov-state').textContent=w;}
|
||||
set('s-keys',fmtN(s.miners_10m!==null&&s.miners_10m!==undefined?s.miners_10m:(d.miners||[]).length));
|
||||
// finality: the last lock, and the rule in plain words
|
||||
var fin=$('fin'),lock=null;if(fn&&fn.checkpoints)fn.checkpoints.forEach(function(c){if(c.state==='locked'&&(!lock||c.index>lock.index))lock=c;});
|
||||
if(fn&&fn.supported&&!fn.active){set('s-lock',fn.latest_locked_index?'#'+fmtN(fn.latest_locked_index):'none');$('s-lock-s').textContent='finality paused';
|
||||
fin.innerHTML='<span class="dot off"></span><span><b>Finality is paused.</b> Under two thirds of the 30-day weight is signing, so no checkpoint locks until it returns. The chain keeps growing; the last lock stands.</span>';}
|
||||
else if(lock){set('s-lock','#'+fmtN(lock.index));var t=lock.locked_at?new Date(String(lock.locked_at).replace(' ','T')).getTime():NaN;$('s-lock-s').textContent=isNaN(t)?'locked':'locked '+rel(now-t);
|
||||
var pct=lock.fraction_total?Math.round(lock.fraction_total*100):(lock.signed&&lock.total?Math.round(lock.signed/lock.total*100):null);
|
||||
fin.innerHTML='<span class="dot live"></span><span><b>Finality is live.</b> Checkpoint #'+fmtN(lock.index)+' locked'+(isNaN(t)?'':' '+rel(now-t))+(pct?' with '+pct+'% of all 30-day weight signing':'')+'. A checkpoint locks when two thirds of all weight signs it, every 30 seconds.</span>';}
|
||||
else{set('s-lock',fn&&fn.supported?'none yet':'no rule');$('s-lock-s').textContent=fn&&fn.supported?'window filling':'';fin.innerHTML='<span class="dot off"></span><span><b>No lock yet.</b> The 30-day weight window is filling; the first checkpoint locks once it is full.</span>';}
|
||||
// miners: top five lanes with a bar each
|
||||
var mel=$('miners');if(d.miners&&d.miners.length){var top=d.miners.slice(0,5),max=Math.max(1,top[0].blocks),rest=d.miners.length-top.length;
|
||||
$('miners-count').textContent=d.miners.length+' vote keys';
|
||||
mel.innerHTML=top.map(function(m){return '<div class="lane"><span class="id">'+esc(m.id)+'</span><div class="bar"><i style="width:'+Math.round(m.blocks/max*100)+'%"></i></div><span class="n">'+m.blocks+' blocks</span></div>';}).join('');
|
||||
$('miners-more').textContent=(rest>0?'and '+rest+' more. ':'')+'Every vote key with a block in the last 10 minutes; a card runs several.';}
|
||||
else{mel.innerHTML='<div class="empty">No blocks in the last 10 minutes.</div>';}
|
||||
// recent blocks: the last eight by time, newest first; a block seen for the first time lands with a glow
|
||||
var list=(d.blocks||[]).slice().sort(function(a,b){return b.ts-a.ts;}).slice(0,8),bel=$('blocks');
|
||||
if(list.length){bel.innerHTML=list.map(function(b){var word=b.locked?'locked':b.proven?'proven':b.color==='red'?'excluded':b.chain?'on the chain':b.color==='blue'?'included':'pending';var fresh=!seen[b.hash];seen[b.hash]=1;
|
||||
return '<div'+(fresh&&Object.keys(seen).length>8?' class="new"':'')+'><span class="num">'+(b.number?fmtN(b.number):esc(b.hash))+'</span><span>'+esc(b.miner||'')+'</span><span class="w'+(word==='locked'||word==='proven'?' hot':'')+'">'+word+'</span></div>';}).join('');}
|
||||
if(window.IgneumYourCard)IgneumYourCard.update(s.hashes_per_second_estimate);
|
||||
lastD=d;sceneCounters(d);if(dag)dag.push(d);
|
||||
var seen={},shardState={},cpState={},evSeen={},evList=[],ready=false,sel=null,lastD=null,dag=null;
|
||||
var mine=(function(){try{return localStorage.getItem('igneum-my-key')||'';}catch(e){return '';}})();
|
||||
document.addEventListener('click',function(e){var t=e.target;if(t&&t.id==='mine-btn'){var v=prompt('Your vote key’s short id, the first 8 hex characters shown in the app or the explorer:',mine||'');if(v!==null){mine=v.trim().toLowerCase();try{if(mine)localStorage.setItem('igneum-my-key',mine);else localStorage.removeItem('igneum-my-key');}catch(x){}remount();if(lastD)render(lastD);}}
|
||||
var row=t&&t.closest?t.closest('tr[data-hash]'):null;if(row&&dag){dag.select(row.getAttribute('data-hash'),'table');}});
|
||||
function render(d){var s=d.state,pv=d.proving,fn=d.finality,now=new Date(d.now).getTime();lastD=d;
|
||||
// the six tiles
|
||||
set('s-bps',s.blocks_per_second_60s!==null&&s.blocks_per_second_60s!==undefined?Number(s.blocks_per_second_60s).toFixed(2):'pending');
|
||||
set('s-blue',fmtN(s.blue_score));set('s-hash',fmtHash(s.hashes_per_second_estimate));
|
||||
set('s-keys',fmtN(s.miners_10m!==null&&s.miners_10m!==undefined?s.miners_10m:(d.miners||[]).length));set('s-peers',fmtN(s.peers));
|
||||
var lock=null;if(fn&&fn.checkpoints)fn.checkpoints.forEach(function(c){if(c.state==='locked'&&(!lock||c.index>lock.index))lock=c;});
|
||||
var le=$('s-lock');if(fn&&fn.supported&&!fn.active){set('s-lock',fn.latest_locked_index?'#'+fmtN(fn.latest_locked_index):'none');le.className='v tick';$('s-lock-d').textContent='finality paused: under two thirds of the weight is signing';}
|
||||
else if(lock){set('s-lock','#'+fmtN(lock.index));le.className='v tick';var t=lock.locked_at?new Date(String(lock.locked_at).replace(' ','T')).getTime():NaN;$('s-lock-d').textContent=(isNaN(t)?'locked':'locked '+rel(now-t))+(lock.fraction_total?' with '+Math.round(lock.fraction_total*100)+'% of all 30-day weight':'');}
|
||||
else{set('s-lock',fn&&fn.supported?'none yet':'no rule');le.className='v tick word';$('s-lock-d').textContent=fn&&fn.supported?'the 30-day weight window is filling':'';}
|
||||
// the graph's count, the blue range, the chain-only view
|
||||
var vr=dag?dag.getViewRange():null,w=dag?dag.getWindow():90,cut=vr?vr.start:now-w*1000,top0=vr?vr.end:now,on=[];(d.blocks||[]).forEach(function(b){if(b.ts>=cut&&b.ts<=top0)on.push(b);});
|
||||
// block activity: the last 30 minutes, one bar a minute, from the observer's per-minute counts
|
||||
var bpm=(s.blocks_per_minute||[]).slice(-30),max=Math.max(1,Math.max.apply(null,bpm.map(function(p){return p[1]||0;}).concat([1])));
|
||||
$('a-count').textContent=on.length;$('a-bars').innerHTML=bpm.map(function(p,i){return '<i style="height:'+Math.max(2,Math.round((p[1]||0)/max*100))+'%" title="'+(p[1]||0)+' blocks"'+(i===bpm.length-1?' class="now"':'')+'></i>';}).join('');
|
||||
var tot30=bpm.reduce(function(a,p){return a+(p[1]||0);},0);$('a-note').textContent=fmtN(tot30)+' blocks in the last '+bpm.length+' min, '+(bpm.length?(tot30/bpm.length).toFixed(1):'0')+' a minute on average; the target is 60.';
|
||||
// mining identities: share of the displayed blocks, the reader's own key marked when known
|
||||
var share={},tot=0;on.forEach(function(b){share[b.miner]=(share[b.miner]||0)+1;tot++;});var ids=Object.keys(share).sort(function(a,b){return share[b]-share[a];}),mel=$('miners');
|
||||
if(ids.length){var top=ids.slice(0,8),rest=ids.length-top.length;mel.innerHTML=top.map(function(id){var n=share[id],pc=n/tot*100;return '<div class="id-row'+(mine&&id===mine?' mine':'')+'"><span class="name"><i class="sw"></i>'+esc(id)+(mine&&id===mine?' (you)':'')+'</span><span class="pct">'+pc.toFixed(1)+'%</span><div class="bar"><i style="width:'+Math.round(pc)+'%"></i></div></div>';}).join('');
|
||||
$('miners-more').textContent=(rest>0?'and '+rest+' more. ':'')+'Vote-key share of the displayed blocks. Several keys may belong to one GPU.';}
|
||||
else mel.innerHTML='<div class="empty">No blocks in the window.</div>';
|
||||
// the event stream: the observer's own events plus what changed between reads
|
||||
var evs=[];(d.events||[]).forEach(function(e){evs.push({ts:new Date(e.ts).getTime(),k:e.kind.replace(/_/g,' '),cls:e.kind==='checkpoint_locked'?'lock':/quiet/.test(e.kind)?'quiet':'',text:e.text});});
|
||||
(d.blocks||[]).forEach(function(b){if(!seen[b.hash]){seen[b.hash]=1;if(ready)evs.push({ts:b.ts,k:'Block observed',cls:'',text:(b.number?'#'+fmtN(b.number):b.hash)+' by '+b.miner});}
|
||||
(b.shards||[]).forEach(function(sh){var key=b.hash+':'+sh.i,prev=shardState[key];shardState[key]=sh.state;if(ready&&prev!==sh.state&&(sh.state==='verified'||sh.state==='paid'))evs.push({ts:now,k:sh.state==='paid'?'Shard paid':'Shard proven',cls:'',text:(sh.i+1)+' of '+sh.n+' for '+(b.number?'#'+fmtN(b.number):b.hash)+(sh.prover?' by '+sh.prover:'')});});});
|
||||
if(fn&&fn.checkpoints)fn.checkpoints.forEach(function(c){var prev=cpState[c.index];cpState[c.index]=c.state;if(ready&&prev!==c.state)evs.push({ts:c.state==='locked'&&c.locked_at?new Date(String(c.locked_at).replace(' ','T')).getTime():now,k:'Checkpoint '+c.state,cls:c.state==='locked'?'lock':'',text:'#'+fmtN(c.index)+(c.fraction_total?', '+Math.round(c.fraction_total*100)+'% of weight':'')});});
|
||||
evs.forEach(function(e){var id=e.k+'|'+e.text+'|'+Math.round(e.ts/1000);if(!evSeen[id]){evSeen[id]=1;evList.unshift({id:id,fresh:ready,ts:e.ts,k:e.k,cls:e.cls,text:e.text});}});
|
||||
evList.sort(function(a,b){return b.ts-a.ts;});evList=evList.slice(0,14);ready=true;
|
||||
var eel=$('events');if(evList.length){eel.innerHTML=evList.map(function(e){var f=e.fresh;e.fresh=false;return '<div'+(f?' class="new"':'')+'><span class="dot '+e.cls+'"></span><span class="what"><b>'+esc(e.k)+'</b> '+esc(e.text)+'</span><span class="t">'+hms(e.ts)+'</span></div>';}).join('');$('ev-count').textContent=evList.length+' recent';}
|
||||
// the table: the displayed blocks, newest first
|
||||
var rows=on.slice().sort(function(a,b){return b.blue_score-a.blue_score;}).slice(0,40);
|
||||
$('tbody').innerHTML=rows.length?rows.map(function(b){var word=b.locked?'locked checkpoint':b.proven?'proven':b.color==='red'?'excluded':b.chain?'selected chain':b.color==='blue'?'included':'pending';return '<tr data-hash="'+esc(b.hash)+'"><td class="num">'+fmtN(b.blue_score)+'</td><td>'+esc(b.hash)+'</td><td>'+esc(b.miner)+'</td><td class="num">'+(b.parents||[]).length+'</td><td>'+word+'</td><td class="num">'+(b.number?fmtN(b.number):'')+'</td></tr>';}).join(''):'<tr><td colspan="6">No blocks in the window.</td></tr>';
|
||||
}
|
||||
// the latest-block cell ticks every second between reads
|
||||
setInterval(function(){if(lastTs){var el=$('s-last');el.textContent=rel(Date.now()-lastTs);}},1000);
|
||||
var c=$('steps');
|
||||
if(c&&window.IgneumFeed&&window.IgneumSteps){var steps=IgneumSteps.mount(c,{caption:cap});IgneumFeed.start({window:300,subs:[steps],onState:onState,onData:onData});}
|
||||
// the DAG below, fed by the same reads
|
||||
var dag=null,lastD=null;
|
||||
function sceneCounters(d){var w=dag?dag.getWindow():120,cut=new Date(d.now).getTime()-w*1000,on=0,chain=0;(d.blocks||[]).forEach(function(b){if(b.ts>=cut){on++;if(b.chain)chain++;}});$('c-screen').textContent=on;$('c-chain').textContent=chain;}
|
||||
function mountDag(){if(dag||!window.IgneumDag)return;dag=IgneumDag.mount($('dag'),{poll:false,window:120,tooltip:$('dag-tip'),chip:$('dag-chip'),pauseButton:$('dag-pause'),
|
||||
onWindow:function(w){$('c-window').textContent=w+' s';},
|
||||
onState:function(st,reason){var el=$('dag-state');el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'the DAG, live':st==='connecting'?'the DAG':esc(reason||st));}});
|
||||
if(lastD)dag.push(lastD);}
|
||||
// the inspector: the pack's selected record, kept fresh on every reply through onSelect(block, 'update'); the proof scene is IgneumProof
|
||||
var proof=null;
|
||||
function inspect(b){sel=b;$('insp-close').hidden=false;var mineB=mine&&b.miner===mine;$('i-kind').textContent=mineB?'your block':b.locked?'checkpoint':b.chain?'chain block':b.color==='red'?'excluded':'side block';
|
||||
$('i-title').textContent=b.locked?'Checkpoint locked':b.proven?'Proof complete':(b.shards||[]).some(function(x){return x.state==='proving';})?'Proof in progress':'Block observed';
|
||||
$('i-hash').textContent=b.hash+(b.number?' · #'+fmtN(b.number):'');
|
||||
var sh=b.shards||[],done=sh.filter(function(x){return x.state==='verified'||x.state==='paid';}).length;
|
||||
$('i-shard-count').textContent=sh.length?done+' / '+sh.length+' complete':'no shard plan yet';$('i-track').innerHTML=sh.map(function(x){return '<i class="'+esc(x.state)+'"></i>';}).join('');
|
||||
$('i-shards').innerHTML=sh.length?sh.map(function(x,i){return '<li class="'+esc(x.state)+'"><span>Shard '+String(i+1).padStart(2,'0')+'</span><span>'+esc(x.state.charAt(0).toUpperCase()+x.state.slice(1))+'</span></li>';}).join(''):'<li class="muted">'+(b.chain?'No shard plan yet.':'Not a chain block: no shards.')+'</li>';
|
||||
$('i-incl').textContent=(b.color==='blue'?'Included':b.color==='red'?'Excluded':'Pending')+(b.chain?' / selected chain':'');
|
||||
$('i-miner').textContent=b.miner+(mineB?' (you)':'');$('i-blue').textContent=fmtN(b.blue_score);$('i-daa').textContent=fmtN(b.daa);
|
||||
$('i-parents').textContent=(b.parents||[]).length+' observed'+((b.parents||[]).length?': '+b.parents.join(', '):'');$('i-time').textContent=hms(b.ts)+' UTC';
|
||||
var fn=lastD&&lastD.finality,cp=null;if(fn&&fn.checkpoints)fn.checkpoints.forEach(function(c){if(b.locked&&c.hash===b.hash)cp=c;});
|
||||
$('i-cp').textContent=cp?'this block, #'+fmtN(cp.index)+(cp.fraction_total?', '+Math.round(cp.fraction_total*100)+'% of weight':''):'not a checkpoint';
|
||||
$('i-lock').textContent=b.final?'Final (reported)':'Not marked final';
|
||||
if(proof)proof.setBlock(b);}
|
||||
function uninspect(){sel=null;$('insp-close').hidden=true;$('i-kind').textContent='select a block';$('i-title').textContent='Select a block';$('i-hash').textContent='Click the graph, or a row of the table below.';$('i-shard-count').textContent='not set';$('i-track').innerHTML='';$('i-shards').innerHTML='<li class="muted">No block selected.</li>';['i-incl','i-miner','i-blue','i-daa','i-parents','i-time','i-cp','i-lock'].forEach(function(id){$(id).textContent='not set';});if(proof)proof.setBlock(null);}
|
||||
$('insp-close').addEventListener('click',function(){if(dag)dag.select(null,'clear');uninspect();});
|
||||
// the four stats above the graph, from the module's own window
|
||||
function miner(st){if(!dag)return;var cps=dag.getCheckpoints(),cp=cps.length?cps[cps.length-1]:null;
|
||||
$('m-blocks').textContent=fmtN(st.blocks);$('m-win').textContent='last '+st.window+' s';$('m-keys').textContent=st.minerKeys+' miner keys · not a card count';$('lane-caption').textContent=st.minerKeys+' keys';
|
||||
$('m-mine').textContent=mine?fmtN(st.own):'0';$('m-mine').classList.toggle('dim',!mine);$('m-key').textContent=mine||'not set';
|
||||
$('m-proven').textContent=fmtN(st.proven);$('m-proven-pct').textContent=st.blocks?Math.round(st.proven/st.blocks*100)+'% of window':'no blocks';
|
||||
$('m-cp').textContent=cp?(cp.state==='locked'?'Locked':'Pending'):'None';$('m-cp-w').textContent=cp&&cp.fraction_total!==null&&cp.fraction_total!==undefined?Math.round(cp.fraction_total*100)+'% weight':'';$('m-cp-note').textContent=cp?'#'+fmtN(cp.index)+' · blue score '+fmtN(cp.blue_score):'no checkpoint in the reply';
|
||||
$('c-count').textContent=st.blocks+' blocks';var r=dag.getViewRange(),lo=null,hi=null;dag.getBlocks().forEach(function(b){if(b.ts>=r.start&&b.ts<=r.end){if(lo===null||b.blue_score<lo)lo=b.blue_score;if(hi===null||b.blue_score>hi)hi=b.blue_score;}});$('blue-range').textContent=lo===null?'blue score pending':'blue score '+fmtN(lo)+' to '+fmtN(hi);}
|
||||
function mountDag(){if(dag||!window.IgneumDag)return;
|
||||
dag=IgneumDag.mount($('dag'),{poll:false,window:60,mine:mine||undefined,tooltip:$('dag-tip'),chip:$('dag-chip'),pauseButton:$('dag-pause'),
|
||||
onWindow:function(w){$('z-pct').textContent=w+' s';if(lastD)render(lastD);},
|
||||
onState:function(st,reason){var el=$('dag-state');el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'live devnet':esc(reason||st));if(proof)proof.setPaused(st!=='live'||dag.getStats().paused);},
|
||||
onPause:function(p){$('dag-follow').classList.toggle('on',!p);if(proof)proof.setPaused(p||dag.getState().state!=='live');},
|
||||
onFollow:function(f){$('dag-follow').classList.toggle('on',f);},
|
||||
onStats:miner,
|
||||
onSelect:function(b,why){if(b){var full=lastD&&(lastD.blocks||[]).filter(function(x){return x.hash===b.hash;})[0];inspect(Object.assign({},b,full?{number:full.number}:{}));}else uninspect();}});
|
||||
if(window.IgneumProof&&!proof)proof=IgneumProof.mount($('proof'));
|
||||
if(lastD)feed(lastD);}
|
||||
function remount(){if(dag){dag.destroy();dag=null;}mountDag();}
|
||||
function feed(d){if(!dag)return;dag.push(d);}
|
||||
if(window.IgneumDag)mountDag();else{var ms=document.querySelector('script[src="/live-dag.js"]');if(ms)ms.addEventListener('load',mountDag);}
|
||||
if(!window.IgneumProof){var ps=document.querySelector('script[src="/proof-core.js"]');if(ps)ps.addEventListener('load',function(){if(dag&&!proof)proof=IgneumProof.mount($('proof'));});}
|
||||
document.querySelectorAll('.tab[data-filter]').forEach(function(t){t.addEventListener('click',function(){document.querySelectorAll('.tab[data-filter]').forEach(function(x){x.classList.toggle('on',x===t);x.setAttribute('aria-pressed',x===t?'true':'false');});if(dag)dag.setFilter(t.getAttribute('data-filter'));});});
|
||||
$('dag-follow').addEventListener('click',function(){if(dag){dag.setPaused(false);dag.follow();}});
|
||||
$('z-in').addEventListener('click',function(){if(dag)dag.setWindow(dag.getWindow()-15);});$('z-out').addEventListener('click',function(){if(dag)dag.setWindow(dag.getWindow()+15);});
|
||||
window.__dbg={render:render,feed:feed,getDag:function(){return dag;}};
|
||||
function setLive(st,reason){var el=$('live-state');el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'live devnet · one node read every 2 s':st==='connecting'?'connecting to the observer':esc(reason||st));}
|
||||
function poll(){fetch('/api/live?window=300',{cache:'no-store'}).then(function(r){if(!r.ok)throw new Error(r.status);return r.json();}).then(function(d){if(!d.ok)throw new Error(d.error);failures=0;setLive(d.state.stale?'stale':'live',d.state.stale?'observer stale':'');render(d);feed(d);}).catch(function(){failures++;if(failures>=3)setLive('failed','live feed unavailable');});}
|
||||
poll();setInterval(function(){if(!document.hidden)poll();},2000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -177,8 +177,8 @@ pre b{color:var(--molten-text);font-weight:500}
|
|||
<div class="grid c2">
|
||||
<div>
|
||||
<div class="dl-list">
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="primary">Windows <span data-dl-meta="miner-windows">v0.3.17 · 62.6 MB</span></a>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg">macOS <span data-dl-meta="miner-mac">v0.3.17 · 41.9 MB</span></a>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="primary">Windows <span data-dl-meta="miner-windows">v0.3.18 · 62.6 MB</span></a>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg">macOS <span data-dl-meta="miner-mac">v0.3.18 · 42.0 MB</span></a>
|
||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz">Linux · HiveOS <span data-dl-meta="miner-hive">v0.3.17 · 24.7 MB</span></a>
|
||||
</div>
|
||||
<p class="notice" data-devnet-notice><b>Devnet.</b> Coins have no value and the chain may be reset.</p>
|
||||
|
|
@ -269,7 +269,7 @@ pre b{color:var(--molten-text);font-weight:500}
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<a class="dl-bar" href="#get"><b>Download the miner</b><span data-dl-meta="miner-windows">v0.3.17 · 62.6 MB</span></a>
|
||||
<a class="dl-bar" href="#get"><b>Download the miner</b><span data-dl-meta="miner-windows">v0.3.18 · 62.6 MB</span></a>
|
||||
|
||||
<!-- footer:start -->
|
||||
<footer class="foot">
|
||||
|
|
|
|||
72
site/proof-core.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/* Igneum proof detail. A data-driven companion, not a mining-progress estimate.
|
||||
* One orbital tile per displayed shard (up to 12). No time-based status promotion.
|
||||
* IgneumProof.mount(canvas).setBlock(observerBlock); destroy() on unmount.
|
||||
*/
|
||||
(function(root){
|
||||
'use strict';
|
||||
var mounted=new WeakMap(),TAU=Math.PI*2;
|
||||
function mount(canvas,opts){
|
||||
opts=opts||{};if(mounted.has(canvas))mounted.get(canvas).destroy();
|
||||
var ctx=canvas.getContext('2d');if(!ctx)throw new Error('A 2D canvas context is required.');
|
||||
var W=0,H=0,dpr=1,block=null,raf=0,disposed=false,paused=false,inView=true,lastPaint=0,frames=0,force=true;
|
||||
var mq=matchMedia('(prefers-reduced-motion: reduce)'),reduce=mq.matches,motion=true,col={},ro,io,mo;
|
||||
var phase=0,lastT=0;
|
||||
function alpha(c,a){if(/^#[a-f\d]{6}$/i.test(c))return 'rgba('+parseInt(c.slice(1,3),16)+','+parseInt(c.slice(3,5),16)+','+parseInt(c.slice(5,7),16)+','+a+')';return c;}
|
||||
function theme(){var s=getComputedStyle(document.documentElement);function c(k,f){return s.getPropertyValue(k).trim()||f;}col={ember:c('--ember','#F2541B'),gold:c('--molten','#FFB35C'),bone:c('--bone','#F4F1EC'),ash:c('--ash','#9A9A9E'),line:c('--line','#2A2A30'),surface:c('--graphite','#16161A'),bg:c('--row','#111114')};kick();}
|
||||
function size(){var nw=canvas.clientWidth,nh=canvas.clientHeight,nd=Math.min(devicePixelRatio||1,2);if(W===nw&&H===nh&&dpr===nd&&canvas.width===Math.round(nw*nd)){kick();return;}W=nw;H=nh;dpr=nd;canvas.width=W*dpr;canvas.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);kick();}
|
||||
function poly(points,fill,stroke){ctx.beginPath();points.forEach(function(p,i){if(i===0)ctx.moveTo(p[0],p[1]);else ctx.lineTo(p[0],p[1]);});ctx.closePath();if(fill){ctx.fillStyle=fill;ctx.fill();}if(stroke){ctx.strokeStyle=stroke;ctx.stroke();}}
|
||||
function iso(x,y,w,h,depth,fill,stroke){
|
||||
poly([[x-w,y],[x,y+h],[x,y+h+depth],[x-w,y+depth]],alpha(fill,.17),alpha(stroke,.4));
|
||||
poly([[x,y+h],[x+w,y],[x+w,y+depth],[x,y+h+depth]],alpha(fill,.07),alpha(stroke,.32));
|
||||
poly([[x,y-h],[x+w,y],[x,y+h],[x-w,y]],col.bg,stroke);
|
||||
poly([[x,y-h],[x+w,y],[x,y+h],[x-w,y]],alpha(fill,.15),null);
|
||||
}
|
||||
function line(a,b,c,w){ctx.strokeStyle=c;ctx.lineWidth=w||1;ctx.beginPath();ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);ctx.stroke();}
|
||||
function draw(t){
|
||||
if(!W||!H||disposed)return;frames++;
|
||||
if(lastT&&!reduce&&motion&&!paused)phase+=Math.min(t-lastT,70)/1000;lastT=t;
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,W,H);
|
||||
var x=W*.5,y=H*.49,scale=Math.min(W/320,H/240),cw=43*scale,ch=23*scale;
|
||||
var shards=block?block.shards||[]:[],active=shards.some(function(s){return s.state==='proving';});
|
||||
var glow=ctx.createRadialGradient(x,y,0,x,y,W*.43);glow.addColorStop(0,alpha(col.ember,.10));glow.addColorStop(1,alpha(col.ember,0));ctx.fillStyle=glow;ctx.fillRect(0,0,W,H);
|
||||
// A stationary coordinate grid, not fabricated hashrate or work counters.
|
||||
for(var k=-3;k<=3;k++){line([x-130*scale,y+k*18*scale-46*scale],[x+130*scale,y+k*18*scale+46*scale],alpha(col.line,.45));line([x-130*scale,y+k*18*scale+46*scale],[x+130*scale,y+k*18*scale-46*scale],alpha(col.line,.45));}
|
||||
ctx.strokeStyle=alpha(col.line,.7);ctx.lineWidth=1;ctx.setLineDash([2,6]);ctx.beginPath();ctx.ellipse(x,y+10*scale,106*scale,59*scale,0,0,TAU);ctx.stroke();ctx.setLineDash([]);
|
||||
var count=Math.min(12,shards.length);
|
||||
for(var i=0;i<count;i++){
|
||||
var angle=-Math.PI/2+i/count*TAU,px=x+Math.cos(angle)*108*scale,py=y+Math.sin(angle)*57*scale;
|
||||
var state=shards[i].state,ink=state==='proving'?col.ember:state==='paid'?col.gold:state==='verified'?col.bone:col.ash;
|
||||
var midx=(px+x)/2,midy=(py+y)/2;
|
||||
ctx.strokeStyle=alpha(ink,state==='planned'?.14:.38);ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(px,py);ctx.quadraticCurveTo(midx,midy-12*scale,x,y);ctx.stroke();
|
||||
if(state==='proving'&&!reduce&&motion&&!paused){var p=(phase*.48+i*.33)%1,xx=(1-p)*(1-p)*px+2*(1-p)*p*midx+p*p*x,yy=(1-p)*(1-p)*py+2*(1-p)*p*(midy-12*scale)+p*p*y;ctx.fillStyle=col.gold;ctx.shadowBlur=8;ctx.shadowColor=col.ember;ctx.beginPath();ctx.arc(xx,yy,1.8*scale,0,TAU);ctx.fill();ctx.shadowBlur=0;}
|
||||
iso(px,py,13*scale,7*scale,5*scale,ink,alpha(ink,.72));
|
||||
if(state==='verified'||state==='paid'){ctx.strokeStyle=ink;ctx.lineWidth=1.2;ctx.beginPath();ctx.moveTo(px-4*scale,py);ctx.lineTo(px-1*scale,py+2*scale);ctx.lineTo(px+4*scale,py-2*scale);ctx.stroke();}
|
||||
else{ctx.fillStyle=alpha(ink,state==='proving'?.9:.4);ctx.fillRect(px-1.5*scale,py-1.5*scale,3*scale,3*scale);}
|
||||
ctx.font='8px ui-monospace, monospace';ctx.textAlign='center';ctx.fillStyle=col.ash;ctx.fillText('S'+String(i+1).padStart(2,'0'),px,py+22*scale);
|
||||
}
|
||||
// Layered block core. Geometry does not imply three GPUs or three proofs.
|
||||
var lift=active&&!reduce&&motion&&!paused?Math.sin(phase*1.7)*1.8*scale:0;
|
||||
ctx.lineWidth=1;
|
||||
iso(x,y+21*scale,cw,ch,10*scale,col.ember,alpha(col.ember,.34));
|
||||
iso(x,y+6*scale,cw,ch,8*scale,col.ember,alpha(col.ember,.55));
|
||||
iso(x,y-11*scale-lift,cw,ch,7*scale,col.ember,alpha(col.ember,.85));
|
||||
var topY=y-11*scale-lift;
|
||||
poly([[x,topY-13*scale],[x+25*scale,topY],[x,topY+13*scale],[x-25*scale,topY]],alpha(col.ember,block&&block.proven?.28:.06),alpha(col.gold,.46));
|
||||
if(block&&block.proven){ctx.strokeStyle=col.gold;ctx.lineWidth=1.8;ctx.beginPath();ctx.moveTo(x-7*scale,topY);ctx.lineTo(x-2*scale,topY+4*scale);ctx.lineTo(x+8*scale,topY-4*scale);ctx.stroke();}
|
||||
else{ctx.fillStyle=col.ember;ctx.fillRect(x-2*scale,topY-2*scale,4*scale,4*scale);}
|
||||
if(block&&block.locked){ctx.strokeStyle=col.gold;ctx.lineWidth=1.2;ctx.beginPath();ctx.ellipse(x,y+12*scale,67*scale,38*scale,0,0,TAU);ctx.stroke();ctx.font='8px ui-monospace, monospace';ctx.fillStyle=col.gold;ctx.textAlign='center';ctx.fillText('LOCKED CHECKPOINT',x,H-12);}
|
||||
else if(!block){ctx.font='10px ui-monospace, monospace';ctx.textAlign='center';ctx.fillStyle=col.ash;ctx.fillText('SELECT A BLOCK',x,H-12);}
|
||||
else if(shards.length>12){ctx.font='9px ui-monospace, monospace';ctx.textAlign='center';ctx.fillStyle=col.ash;ctx.fillText('+ '+(shards.length-12)+' more shards in the list',x,H-12);}
|
||||
}
|
||||
function continuous(){return !disposed&&!document.hidden&&inView&&!reduce&&motion&&!paused&&block&&(block.shards||[]).some(function(s){return s.state==='proving';});}
|
||||
function frame(t){raf=0;if(disposed||document.hidden||!inView)return;if(force||t-lastPaint>=1000/30){draw(t);lastPaint=t;force=false;}if(continuous())raf=requestAnimationFrame(frame);}
|
||||
function kick(){force=true;if(!disposed&&!document.hidden&&inView&&!raf)raf=requestAnimationFrame(frame);}
|
||||
function stop(){if(raf)cancelAnimationFrame(raf);raf=0;lastT=0;}
|
||||
function visibility(){if(document.hidden)stop();else kick();}
|
||||
function reduction(e){reduce=e.matches;stop();kick();}
|
||||
var api={setBlock:function(b){block=b?{hash:b.hash,proven:b.proven===true,locked:b.locked===true,shards:(b.shards||[]).map(function(s){return {state:s.state};})}:null;canvas.setAttribute('aria-label',block?'Block proof detail: '+block.shards.map(function(s,i){return 'shard '+(i+1)+' '+s.state;}).join(', '):'Select a block to inspect its proof shards');kick();},setPaused:function(value){paused=!!value;stop();kick();},setMotion:function(value){motion=!!value;stop();kick();},getStats:function(){return {frames:frames,reducedMotion:reduce||!motion};},destroy:function(){if(disposed)return;disposed=true;stop();ro.disconnect();if(io)io.disconnect();mo.disconnect();root.removeEventListener('resize',size);document.removeEventListener('visibilitychange',visibility);mq.removeEventListener('change',reduction);mounted.delete(canvas);}};
|
||||
ro=new ResizeObserver(size);ro.observe(canvas);if('IntersectionObserver'in root){io=new IntersectionObserver(function(es){inView=es[0].isIntersecting;if(inView)kick();else stop();});io.observe(canvas);}mo=new MutationObserver(theme);mo.observe(document.documentElement,{attributes:true,attributeFilter:['data-theme','style','class']});
|
||||
root.addEventListener('resize',size);document.addEventListener('visibilitychange',visibility);mq.addEventListener('change',reduction);mounted.set(canvas,api);theme();size();return api;
|
||||
}
|
||||
root.IgneumProof={mount:mount,version:'2.0.0'};
|
||||
})(window);
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
--bone:#F4F1EC;--ink-2:#C9C7C2;--ash:#9A9A9E;--ember-ink:#0C0C0E;--ok:#FFB35C;
|
||||
--scrim:rgba(12,12,14,.72);--top-bg:rgba(12,12,14,.86);--shadow:rgba(0,0,0,.6);--hover:rgba(255,255,255,.04);
|
||||
--ember-12:rgba(242,84,27,.12);--ember-40:rgba(242,84,27,.4);--molten-10:rgba(255,179,92,.1);
|
||||
--included:#3B7DD8;--excluded:#B0362B;
|
||||
/* the chrome tokens older page blocks still read */
|
||||
--ground:var(--obsidian);--surface:var(--graphite);--surface-2:var(--row);--ink:var(--bone);--quiet:var(--ash);
|
||||
--ui-bg:var(--top-bg);--ui-menu:var(--obsidian);--ui-ink:var(--bone);--ui-ink-2:var(--ink-2);--ui-ash:var(--ash);--ui-line:var(--line);--ui-line-2:var(--line-2);
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
--bone:#16161A;--ink-2:#3C3C42;--ash:#6B6B70;--ember-ink:#FFFFFF;--ok:#B8731F;
|
||||
--scrim:rgba(244,241,236,.72);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--hover:rgba(0,0,0,.04);
|
||||
--ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);
|
||||
--included:#2A62B8;--excluded:#B0362B;
|
||||
color-scheme:light;
|
||||
}}
|
||||
:root[data-theme="light"]{
|
||||
|
|
@ -40,6 +42,7 @@
|
|||
--bone:#16161A;--ink-2:#3C3C42;--ash:#6B6B70;--ember-ink:#FFFFFF;--ok:#B8731F;
|
||||
--scrim:rgba(244,241,236,.72);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--hover:rgba(0,0,0,.04);
|
||||
--ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);
|
||||
--included:#2A62B8;--excluded:#B0362B;
|
||||
color-scheme:light;
|
||||
}
|
||||
@media (max-width:1000px){:root{--gutter:24px}}
|
||||
|
|
|
|||