Merge remote-tracking branch 'origin/first-block-21' into release-0.3.21
This commit is contained in:
commit
dc3d763822
5 changed files with 179 additions and 61 deletions
|
|
@ -107,14 +107,24 @@ pub struct Ladder {
|
|||
#[serde(default)]
|
||||
pending: Vec<(String, String, u64)>,
|
||||
/// first-block-21 (the project lead, 7 October 2026: "the 'you got your first block' situation only shows for the miner's
|
||||
/// first ever block"): set the moment the first-block card is raised, when the lifetime count the engine
|
||||
/// persists (settings.json accepted_total) crosses from 0 to 1. Kept across restarts, updates and a kept data
|
||||
/// directory; never cleared by a key changing identities or a card being swapped (the count is the machine's,
|
||||
/// not a key's or a card's). A count that starts over on a record carrying the flag (settings.json lost, the
|
||||
/// ladder kept) raises no first card again; a record without the flag whose first block is older than this
|
||||
/// version (count already 1 or more, or first_block_at set) takes the flag at `start_run`.
|
||||
/// first ever block"; main's reading: SEEN once): set when a window has shown the first-block card (the page
|
||||
/// reports it through POST /api/card/seen on display or dismiss), never when it is raised. The card is raised when
|
||||
/// the lifetime count the engine persists (settings.json accepted_total) crosses from 0 to 1 and waits, across
|
||||
/// restarts and updates, until a window shows it: a block found overnight or across an auto-update greets the
|
||||
/// miner the first time they open the app, and after that never again. Kept across restarts, updates and a kept
|
||||
/// data directory; never cleared by a key changing identities or a card being swapped (the count is the
|
||||
/// machine's, not a key's or a card's). A count that starts over on a record carrying the flag (settings.json
|
||||
/// lost, the ladder kept) raises no first card again; a 0.3.20 record (schema 0, no flag) whose first block already
|
||||
/// came (count 1 or more, or first_block_at set) takes the flag at `start_run`, since 0.3.20 showed it every run.
|
||||
#[serde(default)]
|
||||
pub first_block_shown: bool,
|
||||
/// a window has shown the current `milestone` (set with it by `card_seen`); a seen card is dropped at the next
|
||||
/// `start_run`, an unseen one waits for its window
|
||||
#[serde(default)]
|
||||
pub milestone_seen: bool,
|
||||
/// the record's shape: 0 = written by 0.3.20 (no first-block flag), 1 = first-block-21
|
||||
#[serde(default)]
|
||||
pub schema: u32,
|
||||
}
|
||||
|
||||
/// What the dashboard reads (api/state.ladder): the record above summarised at `now`.
|
||||
|
|
@ -158,24 +168,51 @@ impl Ladder {
|
|||
if let Some(d) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(d);
|
||||
}
|
||||
if let Ok(t) = serde_json::to_string_pretty(self) {
|
||||
let mut me = self.clone();
|
||||
me.schema = 1;
|
||||
if let Ok(t) = serde_json::to_string_pretty(&me) {
|
||||
let _ = std::fs::write(path, t);
|
||||
}
|
||||
}
|
||||
|
||||
/// The engine's run begins over this record (first-block-21, 7 October 2026). `accepted_total` is the lifetime
|
||||
/// count settings.json holds. A block card is for the run that raised it: a milestone persisted by an earlier run
|
||||
/// is dropped here, so a restart or an update never shows "your first block" (or block 100) a second time. A
|
||||
/// record from before the flag existed whose first block already came (the count at 1 or more, or first_block_at
|
||||
/// set) takes the flag now, since that card was shown by the version that raised it. Returns true when the
|
||||
/// record changed and wants saving.
|
||||
/// count settings.json holds. A block card a window has shown (`milestone_seen`) is dropped here, so a restart
|
||||
/// or an update never shows "your first block" (or block 100) a second time; a card no window has shown yet
|
||||
/// waits (found overnight, or across an auto-update restart). A 0.3.20 record (schema 0) carries no flag and
|
||||
/// showed its card on every run, so one whose first block already came (the count at 1 or more, or
|
||||
/// first_block_at set) takes the flag now and its card is dropped. Returns true when the record changed and
|
||||
/// wants saving.
|
||||
pub fn start_run(&mut self, accepted_total: u64) -> bool {
|
||||
let mut dirty = false;
|
||||
if self.milestone.is_some() {
|
||||
self.milestone = None;
|
||||
if self.schema == 0 {
|
||||
if accepted_total >= 1 || self.first_block_at > 0.0 {
|
||||
self.first_block_shown = true;
|
||||
self.milestone = None;
|
||||
}
|
||||
self.schema = 1;
|
||||
dirty = true;
|
||||
}
|
||||
if !self.first_block_shown && (accepted_total >= 1 || self.first_block_at > 0.0) {
|
||||
if self.milestone.is_some() && self.milestone_seen {
|
||||
self.milestone = None;
|
||||
self.milestone_seen = false;
|
||||
dirty = true;
|
||||
}
|
||||
dirty
|
||||
}
|
||||
|
||||
/// A window showed (or the user dismissed) the card for the milestone at `count` and `at` (first-block-21): the
|
||||
/// card is spent, and a first-block card sets the lifetime flag. Returns true when the record changed.
|
||||
pub fn card_seen(&mut self, count: u64, at: f64) -> bool {
|
||||
let m = match self.milestone.as_ref() {
|
||||
Some(m) if m.count == count && (m.at - at).abs() < 1.0 => m,
|
||||
_ => return false,
|
||||
};
|
||||
let mut dirty = false;
|
||||
if !self.milestone_seen {
|
||||
self.milestone_seen = true;
|
||||
dirty = true;
|
||||
}
|
||||
if m.kind == "first" && !self.first_block_shown {
|
||||
self.first_block_shown = true;
|
||||
dirty = true;
|
||||
}
|
||||
|
|
@ -232,6 +269,9 @@ impl Ladder {
|
|||
let n = self.days.len() - KEEP_DAYS as usize;
|
||||
self.days.drain(0..n);
|
||||
}
|
||||
// the first-block card: once in the app's life, when the persisted lifetime count crosses from 0 to 1 on a
|
||||
// record with no block and no flag (first-block-21); a count that starts over raises nothing here
|
||||
let first = total == 1 && !self.first_block_shown && self.first_block_at == 0.0;
|
||||
if self.first_block_at == 0.0 {
|
||||
self.first_block_at = now;
|
||||
self.first_block_card = card.to_string();
|
||||
|
|
@ -244,18 +284,14 @@ impl Ladder {
|
|||
}
|
||||
self.blocks.insert(0, BlockFound { hash: hash.clone(), daa, nonce, at: now, card: card.to_string(), card_name: card_name.to_string(), count: total });
|
||||
self.blocks.truncate(KEEP_BLOCKS);
|
||||
// the first-block card: once in the app's life, when the persisted lifetime count crosses from 0 to 1 and the
|
||||
// flag is unset (first-block-21); a count that starts over on a record already shown raises nothing here
|
||||
let first = total == 1 && !self.first_block_shown;
|
||||
if first {
|
||||
self.first_block_shown = true;
|
||||
}
|
||||
let kind = if first { "first" } else if total == 100 { "hundred" } else if total == 1_000 { "thousand" } else if total >= 10_000 && total % 10_000 == 0 { "ten_thousand" } else if new_card && total > 1 { "card" } else { "" };
|
||||
if kind.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let m = Milestone { kind: kind.into(), count: total, card: card.into(), card_name: card_name.into(), hash, daa, at: now };
|
||||
self.milestone = Some(m.clone());
|
||||
self.milestone_seen = false;
|
||||
self.schema = 1;
|
||||
Some(m)
|
||||
}
|
||||
|
||||
|
|
@ -549,11 +585,22 @@ mod tests {
|
|||
assert_eq!(Ladder::load(&dir.join("missing.json")).votes_signed, 0);
|
||||
}
|
||||
|
||||
// ---- first-block-21 (the project lead, 7 October 2026): the first-block card once in the app's life ----
|
||||
// ---- first-block-21 (the project lead, 7 October 2026): the first-block card SEEN once in the app's life ----
|
||||
|
||||
/// The record as 0.3.20 wrote it after the first block: the milestone persisted, no flag field.
|
||||
/// The record as 0.3.20 wrote it after the first block: the milestone persisted, no flag field, no schema.
|
||||
const RECORD_0320: &str = r#"{"first_block_at":1791140918.5,"first_block_card":"nvidia:0:RTX 4070","first_block_hash":"28abaa8c28ffb4c3eab96b5dc202f8328df7038dc7b2653c9e4b7dd6369ae5ce","first_block_daa":38637,"days":[{"day":20730,"blocks":1}],"cards_first":{"nvidia:0:RTX 4070":1791140918.5},"blocks":[{"hash":"28abaa8c28ffb4c3eab96b5dc202f8328df7038dc7b2653c9e4b7dd6369ae5ce","daa":38637,"nonce":"267dd27200a91c80","at":1791140918.5,"card":"nvidia:0:RTX 4070","card_name":"NVIDIA GeForce RTX 4070","count":1}],"votes_signed":2,"last_vote_index":8496,"last_vote_at":1791140950.0,"signed_open":[8496],"last_signed_locked":0,"signed_locked_count":0,"streak_since":1791140930.0,"shards":[],"milestone":{"kind":"first","count":1,"card":"nvidia:0:RTX 4070","card_name":"NVIDIA GeForce RTX 4070","hash":"28abaa8c28ffb4c3eab96b5dc202f8328df7038dc7b2653c9e4b7dd6369ae5ce","daa":38637,"at":1791140918.5},"first_synced_at":1791140000.0,"first_mining_at":1791140100.0,"pending":[]}"#;
|
||||
|
||||
fn round_trip(l: &Ladder) -> Ladder {
|
||||
// one directory per call: the tests run in parallel and a shared path let one test's remove wipe another's file
|
||||
static N: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let dir = std::env::temp_dir().join(format!("igneum-ladder-first-{}-{}", std::process::id(), N.fetch_add(1, std::sync::atomic::Ordering::SeqCst)));
|
||||
let path = dir.join("ladder.json");
|
||||
l.save(&path);
|
||||
let back = Ladder::load(&path);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
back
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_failed_a_second_run_with_the_lifetime_count_at_1_showed_the_first_card_again() {
|
||||
// the known-failed case: before this change a second run loaded the persisted milestone and the dashboard
|
||||
|
|
@ -561,39 +608,64 @@ mod tests {
|
|||
let mut l: Ladder = serde_json::from_str(RECORD_0320).unwrap();
|
||||
assert_eq!(l.milestone.as_ref().map(|m| m.kind.as_str()), Some("first"), "the fixture carries the persisted card");
|
||||
assert!(!l.first_block_shown, "the 0.3.20 record has no flag");
|
||||
assert_eq!(l.schema, 0);
|
||||
assert!(l.start_run(1), "the record changes and wants saving");
|
||||
let s = l.state(1791150000.0, 600.0);
|
||||
assert!(s.milestone.is_none(), "a card raised by an earlier run is never shown again");
|
||||
assert!(s.milestone.is_none(), "a card 0.3.20 showed on every run is never shown again");
|
||||
assert!(s.first_block_shown);
|
||||
assert_eq!(s.first_block_at, 1791140918.5, "the ladder's first-block facts stay");
|
||||
assert_eq!(l.schema, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_first_ever_block_raises_the_card_once_and_sets_the_flag() {
|
||||
fn the_first_ever_block_raises_the_card_and_a_window_showing_it_sets_the_flag() {
|
||||
let mut l = Ladder::default();
|
||||
assert!(!l.start_run(0), "a fresh record changes nothing at start");
|
||||
l.start_run(0);
|
||||
let m = l.on_block(100.0, "a", "RTX 4070", "0x1", 1).unwrap();
|
||||
assert_eq!(m.kind, "first");
|
||||
assert!(!l.first_block_shown, "raised, not yet seen");
|
||||
assert!(!l.card_seen(7, 100.0), "another card's report changes nothing");
|
||||
assert!(l.card_seen(1, 100.0));
|
||||
assert!(l.first_block_shown);
|
||||
assert!(!l.card_seen(1, 100.0), "a second report (the dismiss) changes nothing");
|
||||
let s = l.state(101.0, 600.0);
|
||||
assert!(s.first_block_shown);
|
||||
assert_eq!(s.milestone.as_ref().unwrap().kind, "first");
|
||||
assert_eq!(s.milestone.as_ref().unwrap().kind, "first", "the card stays on screen through this run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_first_block_found_with_no_window_greets_the_miner_at_the_first_open_and_never_again() {
|
||||
// main's case: the block overnight (no window), an engine restart (an auto-update), the first open shows the
|
||||
// card once; a second open (the next run) does not
|
||||
let mut night = Ladder::default();
|
||||
night.start_run(0);
|
||||
night.on_block(100.0, "a", "RTX 4070", "0x1", 1);
|
||||
let mut morning = round_trip(&night);
|
||||
assert!(!morning.start_run(1), "an unseen card waits: nothing changes at start");
|
||||
let s = morning.state(30_000.0, 600.0);
|
||||
assert_eq!(s.milestone.as_ref().unwrap().kind, "first", "the first open shows the card");
|
||||
assert!(!s.first_block_shown);
|
||||
assert!(morning.card_seen(1, 100.0));
|
||||
assert!(morning.first_block_shown);
|
||||
let mut later = round_trip(&morning);
|
||||
assert!(later.start_run(1), "the seen card is dropped");
|
||||
assert!(later.state(60_000.0, 600.0).milestone.is_none(), "a second open shows nothing");
|
||||
assert!(later.first_block_shown);
|
||||
assert!(later.on_block(60_100.0, "a", "RTX 4070", "0x2", 2).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_restart_with_the_count_at_1_shows_nothing_and_block_2_raises_no_card() {
|
||||
let mut run1 = Ladder::default();
|
||||
run1.on_block(100.0, "a", "RTX 4070", "0x1", 1);
|
||||
let dir = std::env::temp_dir().join(format!("igneum-ladder-first-{}", std::process::id()));
|
||||
let path = dir.join("ladder.json");
|
||||
run1.save(&path);
|
||||
let mut run2 = Ladder::load(&path);
|
||||
assert!(run1.card_seen(1, 100.0), "the window was open");
|
||||
let mut run2 = round_trip(&run1);
|
||||
assert!(run2.first_block_shown, "the flag survives the save");
|
||||
assert!(run2.start_run(1), "the persisted card is dropped");
|
||||
assert_eq!(run2.schema, 1);
|
||||
assert!(run2.start_run(1), "the seen card is dropped");
|
||||
assert!(run2.state(200.0, 600.0).milestone.is_none());
|
||||
assert!(run2.on_block(300.0, "a", "RTX 4070", "0x2", 2).is_none(), "block 2 is an ordinary block");
|
||||
assert_eq!(run2.blocks[0].count, 2);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -604,13 +676,16 @@ mod tests {
|
|||
assert!(l.start_run(1));
|
||||
assert!(l.first_block_shown);
|
||||
assert!(l.milestone.is_none());
|
||||
let mut back: Ladder = serde_json::from_str(&serde_json::to_string(&l).unwrap()).unwrap();
|
||||
let mut back = round_trip(&l);
|
||||
assert!(back.first_block_shown);
|
||||
assert_eq!(back.schema, 1);
|
||||
assert!(!back.start_run(1), "a new-shape record with nothing pending changes nothing");
|
||||
assert!(back.on_block(1791150000.0, "nvidia:0:RTX 4070", "NVIDIA GeForce RTX 4070", "0x3", 2).is_none());
|
||||
// a 0.3.20 record whose count is still 0 but whose first_block_at is set (settings.json rewritten) is shown too
|
||||
let mut l2: Ladder = serde_json::from_str(RECORD_0320).unwrap();
|
||||
assert!(l2.start_run(0));
|
||||
assert!(l2.first_block_shown);
|
||||
assert!(l2.milestone.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -620,11 +695,13 @@ mod tests {
|
|||
l.mark("synced", 10.0);
|
||||
l.mark("mining", 20.0);
|
||||
l.on_vote(30.0, 5, 600.0);
|
||||
assert!(!l.start_run(0), "nothing to reconcile: no block yet");
|
||||
assert!(!l.first_block_shown);
|
||||
let m = l.on_block(100.0, "a", "RTX 4070", "0x1", 1).unwrap();
|
||||
let mut kept = round_trip(&l);
|
||||
assert!(!kept.start_run(0), "nothing to reconcile: no block yet");
|
||||
assert!(!kept.first_block_shown);
|
||||
let m = kept.on_block(100.0, "a", "RTX 4070", "0x1", 1).unwrap();
|
||||
assert_eq!(m.kind, "first");
|
||||
assert!(l.first_block_shown);
|
||||
assert!(kept.card_seen(1, 100.0));
|
||||
assert!(kept.first_block_shown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -632,16 +709,23 @@ mod tests {
|
|||
// settings.json lost (the count back at 0), the ladder kept with the flag: the next block is total 1 again
|
||||
let mut l = Ladder::default();
|
||||
l.on_block(100.0, "a", "RTX 4070", "0x1", 1);
|
||||
l.card_seen(1, 100.0);
|
||||
assert!(l.on_block(200.0, "a", "RTX 4070", "0x2", 1).is_none(), "no second first-block card");
|
||||
assert!(l.on_block(300.0, "b", "RTX 5090", "0x3", 1).is_none(), "nor a new-card card at a count of 1");
|
||||
assert!(l.first_block_shown);
|
||||
assert_eq!(l.state(301.0, 600.0).blocks.len(), 3, "the blocks are still recorded");
|
||||
// the same with the card still unseen: the waiting card stays the one card, no second is raised
|
||||
let mut u = Ladder::default();
|
||||
u.on_block(100.0, "a", "RTX 4070", "0x1", 1);
|
||||
assert!(u.on_block(200.0, "a", "RTX 4070", "0x2", 1).is_none());
|
||||
assert_eq!(u.milestone.as_ref().unwrap().at, 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_2_is_an_ordinary_block_with_no_card() {
|
||||
let mut l = Ladder::default();
|
||||
l.on_block(100.0, "a", "RTX 4070", "0x1", 1);
|
||||
l.card_seen(1, 100.0);
|
||||
assert!(l.on_block(130.0, "a", "RTX 4070", "0x2", 2).is_none());
|
||||
let s = l.state(131.0, 600.0);
|
||||
assert_eq!(s.blocks[0].count, 2);
|
||||
|
|
|
|||
|
|
@ -467,6 +467,15 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
// Read-only: nothing about mining changes. A caller without a host (a bare engine) gets the path and no file.
|
||||
// miner-ui-5: the block card's PNG, drawn by the page on a canvas (no network call), written under
|
||||
// <data root>/cards/ and the folder opened for the user. The body is {name, png: "data:image/png;base64,..."}.
|
||||
// first-block-21: the page showed (or the user dismissed) the block card for the milestone at {count, at};
|
||||
// the card is spent for later runs and a first-block card sets the lifetime flag (src/ladder.rs card_seen)
|
||||
"/api/card/seen" => {
|
||||
let count = body.get("count").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
let at = body.get("at").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let changed = shared.ladder.lock().unwrap().card_seen(count, at);
|
||||
if changed { shared.save_ladder(); }
|
||||
Ok(json!({ "ok": true, "changed": changed }))
|
||||
}
|
||||
"/api/card" => {
|
||||
let name = body.get("name").and_then(|v| v.as_str()).unwrap_or("block");
|
||||
let png = body.get("png").and_then(|v| v.as_str()).ok_or("png missing")?;
|
||||
|
|
|
|||
|
|
@ -1081,16 +1081,21 @@ var View = (function () {
|
|||
s = s || {}; var m = s.mining || {}, ld = s.ladder || {};
|
||||
return m.state === 'mining' && !(m.accepted_total > 0) && !(ld.first_block_at > 0) && !ld.first_block_shown;
|
||||
}
|
||||
// a card is for the run that raised it (first-block-21, the project lead, 7 October 2026: "the 'you got your first block'
|
||||
// situation only shows for the miner's first ever block"). The engine drops a persisted milestone at start
|
||||
// (ladder.rs start_run) and reports the run's start as started_at; against an older engine that still sends one, a
|
||||
// milestone raised before this run began is refused here (a minute of slack; uptime_s stands in when the engine
|
||||
// has no started_at). So a restart, an update or a reinstall over a kept state never shows the card twice.
|
||||
// a card is seen once (first-block-21, the project lead, 7 October 2026: "the 'you got your first block' situation only shows
|
||||
// for the miner's first ever block"; main's reading: seen once). An engine of 0.3.21 or later owns the rule: it
|
||||
// keeps a card no window has shown across restarts and updates (a block found overnight greets the miner at the
|
||||
// first open) and drops a seen one at its next start (ladder.rs start_run, the page's POST api/card/seen), so
|
||||
// whatever such an engine sends is shown. A 0.3.20 engine (its ladder has no first_block_shown field) re-sends the
|
||||
// persisted card on every run: against it a milestone raised before this run began is refused here (a minute of
|
||||
// slack; uptime_s stands in when the engine has no started_at).
|
||||
function staleCard(m, s, now) {
|
||||
if (!m || !(m.at > 0) || !s) return false;
|
||||
if (s.ladder && s.ladder.first_block_shown !== undefined) return false;
|
||||
var started = s.started_at > 0 ? s.started_at : (s.uptime_s >= 0 && now > 0 ? now - s.uptime_s : 0);
|
||||
return started > 0 && m.at + 60 < started;
|
||||
}
|
||||
// the body the page sends to api/card/seen for a card it displays or the user dismisses (null for no card)
|
||||
function cardSeenBody(bc) { return bc && bc.count > 0 ? { count: bc.count, at: bc.when || 0 } : null; }
|
||||
function blockCard(m, s, chain, now) {
|
||||
if (!m || !(m.count > 0)) return null;
|
||||
if (staleCard(m, s, now)) return null;
|
||||
|
|
@ -1192,7 +1197,7 @@ var View = (function () {
|
|||
}
|
||||
function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? Mining restarts.'; }
|
||||
function trustAsk(on) { return on ? 'Include proofs the node never checked? Devnet only. The node restarts.' : 'Check proofs again? The node restarts.'; }
|
||||
return { VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, driverWords: driverWords, vendorWord: vendorWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveInsteadWords: proveInsteadWords, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, firstWait: firstWait, staleCard: staleCard, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords };
|
||||
return { VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, driverWords: driverWords, vendorWord: vendorWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveInsteadWords: proveInsteadWords, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, firstWait: firstWait, staleCard: staleCard, cardSeenBody: cardSeenBody, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords };
|
||||
})();
|
||||
/* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ----------
|
||||
One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned
|
||||
|
|
@ -1960,7 +1965,15 @@ if (typeof document !== 'undefined') (function () {
|
|||
disclosure('tl-toggle', 'tl-details');
|
||||
$('s-profile').addEventListener('change', function () { var on = this.checked; api('api/settings', { profile_public: on }).then(function (r) { if (r.ok) toast(on ? 'Your page is public from the next site read' : 'Your page is unlisted again'); else { toast(r.error || 'could not change'); $('s-profile').checked = !on; } }).catch(function () { toast('This engine has no profile switch yet'); $('s-profile').checked = !on; }); });
|
||||
// the block card's buttons
|
||||
$('bc-close').addEventListener('click', function () { cardDismissed = $('block-card').dataset.key || ''; store('igneum.card.dismissed', cardDismissed); $('block-card').hidden = true; });
|
||||
// first-block-21: the card is spent once a window has shown it. The first render in a visible window reports it to
|
||||
// the engine (api/card/seen: the lifetime flag, the drop at the engine's next start); the dismiss reports it too; and
|
||||
// the window hiding to the tray or menu bar with the card on screen counts as the dismiss, so a second open in the
|
||||
// same run shows nothing either.
|
||||
var cardSeenKey = '';
|
||||
function reportCardSeen(bc) { if (!bc || cardSeenKey === bc.key) return; cardSeenKey = bc.key; var b = View.cardSeenBody(bc); if (b) api('api/card/seen', b).catch(function () { }); }
|
||||
function dismissCard() { cardDismissed = $('block-card').dataset.key || ''; store('igneum.card.dismissed', cardDismissed); $('block-card').hidden = true; }
|
||||
$('bc-close').addEventListener('click', function () { if (state && state.ladder) reportCardSeen(View.blockCard(state.ladder.milestone, state, chain, state.now)); dismissCard(); });
|
||||
document.addEventListener('visibilitychange', function () { if (document.hidden && !$('block-card').hidden && $('block-card').dataset.key) dismissCard(); });
|
||||
$('bc-open').addEventListener('click', function () { var u = $('block-card').dataset.url; if (u) api('api/open', { url: u }); else toast('The hash is not known yet'); });
|
||||
$('bc-copy').addEventListener('click', function () { var u = $('block-card').dataset.url; if (u) copyText(u, 'Link copied'); else toast('The hash is not known yet'); });
|
||||
$('bc-save').addEventListener('click', function () {
|
||||
|
|
@ -2024,6 +2037,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var show = !!bc && bc.key !== cardDismissed;
|
||||
setHidden('block-card', !show);
|
||||
if (show) {
|
||||
if (!hiddenNow()) reportCardSeen(bc);
|
||||
card.dataset.key = bc.key; card.dataset.url = bc.url;
|
||||
setText('bc-eyebrow', bc.eyebrow); setText('bc-when', bc.when > 0 ? clock(bc.when) : ''); setText('bc-title', bc.title);
|
||||
$('bc-card').innerHTML = View.markHtml(bc.card, 'mini') + '<span>' + esc(bc.cardLine) + '</span>'; $('bc-card').title = bc.fields.card;
|
||||
|
|
|
|||
|
|
@ -645,41 +645,52 @@ test('a Devnet 2 key walks every rung on screen, and every rung names its RPC fi
|
|||
assert.equal(V.ladderRungs(stateOf(), chainOf(), T5)[6].line, 'Proving is off.');
|
||||
});
|
||||
|
||||
test('first-block-21 (the project lead, 7 October 2026): the first-block card shows once in the app’s life, never on a later run', () => {
|
||||
test('first-block-21 (the project lead, 7 October 2026): the first-block card is seen once in the app’s life, never on a later run', () => {
|
||||
const m1 = { kind: 'first', count: 1, card: 'nvidia:0:RTX 4070', card_name: 'NVIDIA GeForce RTX 4070', hash: '9f3a' + 'ab'.repeat(28) + 'c21e', daa: 1284117, at: T5 - 60 };
|
||||
const c = chainOf({ mine: [key({ blocks: 1, rank: 4 })], best: key({ blocks: 1, rank: 4 }) });
|
||||
const afterFirst = (over, ladderOver) => stateOf({ mining: { ...stateOf().mining, accepted_total: 1, accepted_session: 0, found: [] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 7200, first_block_card: 'nvidia:0:RTX 4070', first_block_hash: m1.hash, first_block_daa: 1284117, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, first_block_shown: true, ...ladderOver }, ...over });
|
||||
// KNOWN-FAILED FIRST: a second run (the engine up 30 s, the lifetime count already 1, the card raised two hours ago
|
||||
// on the first run and still in the record an older engine sends) showed "your first block" again before this change
|
||||
const stale = { ...m1, at: T5 - 7200 };
|
||||
// KNOWN-FAILED FIRST: a second run against a 0.3.20 engine (its ladder has no first_block_shown field; the engine up
|
||||
// 30 s, the lifetime count already 1, the card raised two hours ago on the first run and re-sent from its record)
|
||||
// showed "your first block" again before this change
|
||||
let s = afterFirst({ uptime_s: 30, started_at: T5 - 30 }, { milestone: stale });
|
||||
delete s.ladder.first_block_shown;
|
||||
assert.equal(V.blockCard(stale, s, c, T5), null, 'a second run with the count at 1 shows no first-block card');
|
||||
assert.equal(V.staleCard(stale, s, T5), true);
|
||||
// the first ever block: the card, in the run that raised it
|
||||
s = stateOf({ mining: { ...stateOf().mining, accepted_total: 1, accepted_session: 1, found: [T5 - 60] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 60, first_block_card: 'nvidia:0:RTX 4070', first_block_hash: m1.hash, first_block_daa: 1284117, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, first_block_shown: true, milestone: m1 } });
|
||||
s.started_at = T5 - 1800;
|
||||
// the first ever block: the card, in the run that raised it, and the body the page reports when it displays it
|
||||
s = stateOf({ started_at: T5 - 1800, mining: { ...stateOf().mining, accepted_total: 1, accepted_session: 1, found: [T5 - 60] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 60, first_block_card: 'nvidia:0:RTX 4070', first_block_hash: m1.hash, first_block_daa: 1284117, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, first_block_shown: false, milestone: m1 } });
|
||||
const bc = V.blockCard(m1, s, c, T5);
|
||||
assert.equal(bc.eyebrow, 'your first block');
|
||||
assert.equal(bc.title, 'Block 1,284,117 is yours.');
|
||||
assert.equal(V.staleCard(m1, s, T5), false);
|
||||
assert.deepEqual(V.cardSeenBody(bc), { count: 1, at: T5 - 60 });
|
||||
assert.equal(V.cardSeenBody(null), null);
|
||||
assert.equal(V.firstWait(s), false, 'the count-up gives way to the card');
|
||||
// a restart with the count at 1: the engine dropped the milestone (ladder.rs start_run); nothing to show, the
|
||||
// first rung stays done and the count-up never comes back
|
||||
// seen once (main's reading): a block found overnight with no window, the engine restarted since (an auto-update):
|
||||
// a 0.3.21 engine keeps the unseen card and the first open shows it, however old it is
|
||||
s = afterFirst({ uptime_s: 30, started_at: T5 - 30 }, { first_block_shown: false, milestone: stale });
|
||||
assert.equal(V.staleCard(stale, s, T5), false, 'a 0.3.21 engine owns the rule');
|
||||
assert.equal(V.blockCard(stale, s, c, T5).eyebrow, 'your first block');
|
||||
// a second open after it was seen: the engine dropped the card at its start (ladder.rs start_run); the first rung
|
||||
// stays done and the count-up never comes back
|
||||
s = afterFirst({ uptime_s: 12, started_at: T5 - 12 }, { milestone: null });
|
||||
assert.equal(V.blockCard(s.ladder.milestone, s, c, T5), null);
|
||||
assert.equal(V.ladderRungs(s, c, T5)[0].state, 'done');
|
||||
assert.equal(V.firstWait(s), false);
|
||||
// an update with the count at 1: a newer version, the same rule; a stale card an older engine still sends is refused
|
||||
s = afterFirst({ uptime_s: 45, started_at: T5 - 45, version: '0.3.21' }, { milestone: stale });
|
||||
assert.equal(V.blockCard(stale, s, c, T5), null);
|
||||
// an engine without started_at (older): uptime_s stands in
|
||||
// a restart with the count at 1 and the card seen: nothing to show
|
||||
s = afterFirst({ uptime_s: 45, started_at: T5 - 45 }, { milestone: null });
|
||||
assert.equal(V.blockCard(s.ladder.milestone, s, c, T5), null);
|
||||
// an update with the count at 1 from 0.3.20: the engine takes the flag from the old record and sends no card; a
|
||||
// 0.3.20 engine without started_at re-sending its card is refused through uptime_s
|
||||
s = afterFirst({ uptime_s: 45, started_at: T5 - 45, version: '0.3.21' }, { milestone: null });
|
||||
assert.equal(V.blockCard(s.ladder.milestone, s, c, T5), null);
|
||||
s = afterFirst({ uptime_s: 45, started_at: 0 }, { milestone: stale });
|
||||
delete s.ladder.first_block_shown;
|
||||
assert.equal(V.blockCard(stale, s, c, T5), null);
|
||||
// a kept data directory with the count at 0 and the flag unset: the count-up shows, then the first block raises the card
|
||||
s = stateOf({ uptime_s: 300, started_at: T5 - 300, ladder: { ...stateOf().ladder, first_block_shown: false, first_synced_at: T5 - 86400 * 3, first_mining_at: T5 - 86400 * 3, votes_signed: 40 } });
|
||||
assert.equal(V.firstWait(s), true);
|
||||
assert.equal(V.ladderRungs(s, chainOf(), T5)[0].line, 'Waiting for your first block.');
|
||||
s = stateOf({ uptime_s: 400, started_at: T5 - 400, mining: { ...stateOf().mining, accepted_total: 1, accepted_session: 1, found: [T5 - 5] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 5, first_block_shown: true, first_synced_at: T5 - 86400 * 3, first_mining_at: T5 - 86400 * 3, votes_signed: 40, milestone: { ...m1, at: T5 - 5 } } });
|
||||
s = stateOf({ uptime_s: 400, started_at: T5 - 400, mining: { ...stateOf().mining, accepted_total: 1, accepted_session: 1, found: [T5 - 5] }, ladder: { ...stateOf().ladder, first_block_at: T5 - 5, first_block_shown: false, first_synced_at: T5 - 86400 * 3, first_mining_at: T5 - 86400 * 3, votes_signed: 40, milestone: { ...m1, at: T5 - 5 } } });
|
||||
assert.equal(V.blockCard(s.ladder.milestone, s, c, T5).eyebrow, 'your first block');
|
||||
assert.equal(V.firstWait(s), false);
|
||||
// a count that started over (settings.json lost, the ladder kept with the flag): no "waiting", no card
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
// fresh setup not done (welcome, cards, address screens)
|
||||
// job a remote job running
|
||||
// firstwait miner-ui-5: mining, no block yet (the Poisson count-up), the ladder's first rung
|
||||
// firstblock miner-ui-5: the first block a minute ago (the block card), the key under the dust line
|
||||
// firstblock miner-ui-5: the first block a minute ago (the block card, not yet seen by a window), the key under the dust line
|
||||
// secondblock first-block-21: block 2 two seconds ago, ten minutes after the first; the ordinary accepted pulse (the
|
||||
// block on the strip, the activity line, "2 blocks this run"), no card; ladder.first_block_shown true
|
||||
// ladder miner-ui-5: every rung walked (vote, signature, full window, rank 3, streak, a paid shard)
|
||||
|
|
@ -125,7 +125,7 @@ function ladderNow(t) {
|
|||
const hash = '9f3a7c1e5d2b4a6f8e0c1d3b5a7f9e2c4d6b8a0f1e3c5d7b9a2f4e6c8d0b1a3c21e';
|
||||
const hash2 = '4c1d9e7a2b5f8c0e3d6a9b1c4e7f0a2d5b8c1e4f7a0b3d6c9e2f5a8b1d4c7e0f3a2';
|
||||
if (scenario === 'secondblock') return { ...base, first_block_at: t - 602, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 2, blocks_today: 2, cards_first: { [recState.mining.cards[0].key]: t - 602 }, blocks: [{ hash: hash2, daa: 267054, nonce: '3a9c0d11e2f4b5a6', at: t - 2, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 2 }, { hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 602, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 20, last_vote_index: 8516, last_vote_at: t - 8, milestone: null, first_block_shown: true };
|
||||
if (scenario === 'firstblock') return { ...base, first_block_at: t - 70, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, blocks: [{ hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 70, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 2, last_vote_index: 8496, last_vote_at: t - 20, milestone: { kind: 'first', count: 1, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266454, at: t - 70 }, first_block_shown: true };
|
||||
if (scenario === 'firstblock') return { ...base, first_block_at: t - 70, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, blocks: [{ hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 70, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 2, last_vote_index: 8496, last_vote_at: t - 20, milestone: { kind: 'first', count: 1, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266454, at: t - 70 }, first_block_shown: false };
|
||||
if (scenario === 'ladder') return { ...base, first_block_at: t - 7300, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 259200, days_mined_30: 1, blocks_30d: 61, blocks_today: 61, votes_signed: 240, last_vote_index: 8495, last_vote_at: t - 12, last_signed_locked: 8495, signed_locked_count: 238, streak_s: 7100, shards: [{ block: 160390, shard: 3, wei: '1150000000000000000', at: t - 400 }], milestone: { kind: 'hundred', count: 100, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266400, at: t - 50 }, first_block_shown: true };
|
||||
return base;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue