Horizon polish for 0.3.16: (Q4) fork_is_close treats a passed activation as not close (every update since the 0.3.14 manifest read it as 0 blocks away and skipped every guard), publish-manifest.sh refuses an activation height at or below the live DAA unless --allow-passed-activation; (Q83, Q84, Q2) the finality pause shown: state.finality.paused, paused_since, reason, held_by, line ("Finality paused since 18:39 UTC: under two thirds of the weight is signing", the node's cause when it carries finality_reason/held_by), on the node line, the Overview's state and the Finality card, no lock called final while paused; tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
2db775f1db
commit
c040eabfb9
8 changed files with 135 additions and 7 deletions
|
|
@ -906,6 +906,41 @@ pub fn tuned_line(mhs: f64, watts: f64, eff: f64) -> String {
|
|||
format!("Tuned: {mhs:.1} MH/s at {watts:.0} W ({eff:.3} MH/W)")
|
||||
}
|
||||
|
||||
/// HH:MM UTC of a unix time (the day is not shown: "since 18:39 UTC").
|
||||
pub fn hhmm_utc(unix: f64) -> String {
|
||||
let s = unix.max(0.0) as u64 % 86_400;
|
||||
format!("{:02}:{:02}", s / 3600, (s % 3600) / 60)
|
||||
}
|
||||
|
||||
/// Horizon polish Q83: the one sentence every surface shows while finality is paused. The cause is the node's own
|
||||
/// (`reason`, with who holds it) when it carries one, else the plain two-thirds line; never the word "final".
|
||||
pub fn finality_paused_line(since_unix: f64, reason: &str, held_by: &str) -> String {
|
||||
let cause = if reason.trim().is_empty() {
|
||||
"under two thirds of the weight is signing".to_string()
|
||||
} else if held_by.trim().is_empty() {
|
||||
reason.trim().to_string()
|
||||
} else {
|
||||
format!("{} (held by {})", reason.trim(), held_by.trim())
|
||||
};
|
||||
format!("Finality paused since {} UTC: {cause}", hhmm_utc(since_unix))
|
||||
}
|
||||
|
||||
/// The node's pause line, when it carries one: `... finality_reason=<words or "quoted words"> held_by=<id>`.
|
||||
/// Returns (reason, held_by); None when the line is not one.
|
||||
pub fn parse_finality_line(text: &str) -> Option<(String, String)> {
|
||||
let i = text.find("finality_reason=")?;
|
||||
let rest = &text[i + "finality_reason=".len()..];
|
||||
let (reason, after) = if let Some(q) = rest.strip_prefix('"') {
|
||||
let end = q.find('"')?;
|
||||
(q[..end].to_string(), &q[end + 1..])
|
||||
} else {
|
||||
let end = rest.find(' ').unwrap_or(rest.len());
|
||||
(rest[..end].replace('_', " "), &rest[end..])
|
||||
};
|
||||
let held_by = after.find("held_by=").map(|j| after[j + 8..].split_whitespace().next().unwrap_or("").to_string()).unwrap_or_default();
|
||||
Some((reason, held_by))
|
||||
}
|
||||
|
||||
/// Miner UI 4 (6 October 2026): the fleet's draw now: the sum over cards that mine and have a draw reading under
|
||||
/// `max_age_s` old (a stale reading is not a draw).
|
||||
pub fn fleet_watts<'a>(cards: impl Iterator<Item = (&'a str, f64, f64)>, now: f64, max_age_s: f64) -> f64 {
|
||||
|
|
@ -1063,6 +1098,20 @@ mod tests {
|
|||
assert_eq!(wei_from_hex("soon"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_finality_pause_line_names_the_time_and_the_cause_and_never_says_final() {
|
||||
// 6 October 2026 18:39:00Z
|
||||
let since = 1_791_311_940.0;
|
||||
assert_eq!(hhmm_utc(since), "18:39");
|
||||
let plain = finality_paused_line(since, "", "");
|
||||
assert_eq!(plain, "Finality paused since 18:39 UTC: under two thirds of the weight is signing");
|
||||
assert!(!plain.to_ascii_lowercase().contains("final "), "no 'final' word while paused: {plain}");
|
||||
assert_eq!(finality_paused_line(since, "a checkpoint vote is split", "ae432dc7"), "Finality paused since 18:39 UTC: a checkpoint vote is split (held by ae432dc7)");
|
||||
assert_eq!(parse_finality_line("1791311940 finality paused finality_reason=\"vote split at index 412\" held_by=ae432dc7"), Some(("vote split at index 412".into(), "ae432dc7".into())));
|
||||
assert_eq!(parse_finality_line("x finality_reason=under_two_thirds"), Some(("under two thirds".into(), String::new())));
|
||||
assert_eq!(parse_finality_line("status: accepted 3 blocks"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_card_goal_overrides_the_global_goal_and_empty_follows_it() {
|
||||
assert_eq!(goal_for("", "balanced"), Goal::Balanced);
|
||||
|
|
|
|||
|
|
@ -3095,7 +3095,7 @@ impl Engine {
|
|||
node_synced: st.node.synced && st.clock.severity != "block",
|
||||
// Horizon frontier lane: finality paused = a synced node with no checkpoint lock for FINALITY_PAUSE_S
|
||||
// (the last LOCK line's age; or, when none was ever seen this run, the engine's own uptime)
|
||||
finality_paused: st.node.synced && (if st.finality.last_lock > 0 { st.finality.age_s } else { st.uptime_s as f64 }) > crate::manifest::FINALITY_PAUSE_S,
|
||||
finality_paused: st.finality.paused,
|
||||
boundary_eta_s: if st.node.daa > 0 && st.program.boundary_daa > 0 { Some(st.program.eta_s) } else { None },
|
||||
// a remote job in progress counts as busy: no update applies under it (src/jobrun.rs)
|
||||
miner_busy: self.miners.iter().any(|m| m.building) || st.mining.cards.iter().any(|c| c.enabled && c.state == "starting") || self.jobs.active(),
|
||||
|
|
@ -3624,6 +3624,18 @@ impl Engine {
|
|||
st.finality.age_s = unix - st.finality.last_lock_at;
|
||||
st.finality.message = String::new();
|
||||
}
|
||||
// Horizon polish Q83/Q84: finality paused = a synced node with no checkpoint lock for FINALITY_PAUSE_S (the
|
||||
// last lock's age, else the engine's uptime); since the last lock (else the start); one sentence everywhere
|
||||
let gap = if st.finality.last_lock > 0 { st.finality.age_s } else { st.uptime_s as f64 };
|
||||
let paused = st.node.synced && gap > crate::manifest::FINALITY_PAUSE_S;
|
||||
if paused && !st.finality.paused {
|
||||
st.finality.paused_since = if st.finality.last_lock > 0 { st.finality.last_lock_at } else { unix - st.uptime_s as f64 };
|
||||
}
|
||||
st.finality.paused = paused;
|
||||
st.finality.line = if paused { crate::ember::finality_paused_line(st.finality.paused_since, &st.finality.reason, &st.finality.held_by) } else { String::new() };
|
||||
if paused {
|
||||
st.finality.message = st.finality.line.clone();
|
||||
}
|
||||
if self.running {
|
||||
let mining_now = st.mining.cards.iter().any(|c| c.state == "mining");
|
||||
let any_slot = !self.miners.is_empty();
|
||||
|
|
@ -4029,6 +4041,13 @@ impl Engine {
|
|||
st.finality.age_s = 0.0;
|
||||
}
|
||||
}
|
||||
} else if text.contains("finality_reason=") {
|
||||
// the node lane's pause line (0.3.16): the cause and who holds it, shown in the one sentence
|
||||
if let Some((reason, held_by)) = crate::ember::parse_finality_line(text) {
|
||||
let mut st = self.st();
|
||||
st.finality.reason = reason;
|
||||
st.finality.held_by = held_by;
|
||||
}
|
||||
} else if text.contains(" VOTE index=") {
|
||||
self.st().finality.votes += 1;
|
||||
} else if text.contains("worker could not prepare") || text.contains(" prepare-failed ") {
|
||||
|
|
|
|||
|
|
@ -366,7 +366,10 @@ pub fn safe_to_apply(m: &Moment) -> Result<(), String> {
|
|||
/// A consensus activation is within FORK_URGENT_BLOCKS of the node's DAA score (and the node has a score).
|
||||
pub fn fork_is_close(activation_height: Option<u64>, daa: u64) -> bool {
|
||||
match activation_height {
|
||||
Some(h) if daa > 0 => daa.saturating_add(FORK_URGENT_BLOCKS) >= h,
|
||||
// Horizon polish Q4 (6 October 2026): an activation at or below the DAA has PASSED, nothing is pending; the
|
||||
// old rule read it as close, so every update since the 0.3.14 manifest said "0 blocks away, installing now",
|
||||
// stripped Later and skipped every safe-moment guard (PC 1's 17:52:54Z install under a job came through it)
|
||||
Some(h) if daa > 0 && h > daa => daa.saturating_add(FORK_URGENT_BLOCKS) >= h,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -555,8 +558,11 @@ mod tests {
|
|||
assert!(!fork_is_close(Some(120_000), 0));
|
||||
assert!(!fork_is_close(Some(120_000), 118_199));
|
||||
assert!(fork_is_close(Some(120_000), 118_200));
|
||||
assert!(fork_is_close(Some(120_000), 120_000));
|
||||
assert!(fork_is_close(Some(120_000), 130_000));
|
||||
assert!(fork_is_close(Some(120_000), 119_999), "one block before the activation");
|
||||
// a passed activation is not close (Horizon polish Q4): at the height and after it, nothing is pending
|
||||
assert!(!fork_is_close(Some(120_000), 120_000));
|
||||
assert!(!fork_is_close(Some(120_000), 130_000));
|
||||
assert!(!fork_is_close(Some(33_000), 201_776), "the 0.3.14 manifest's case: difficulty v2 at 33,000 against PC 1's DAA");
|
||||
let m = parse(SAMPLE).unwrap();
|
||||
assert!(!unsupported(&m, "0.3.0"));
|
||||
assert!(unsupported(&m, "0.2.9"));
|
||||
|
|
|
|||
|
|
@ -257,6 +257,15 @@ pub struct FinalityState {
|
|||
pub age_s: f64,
|
||||
pub votes: u64,
|
||||
pub message: String,
|
||||
/// Horizon polish Q83/Q84 (6 October 2026): the network's finality is paused (a synced node, no checkpoint lock
|
||||
/// for manifest::FINALITY_PAUSE_S); since when (the last lock's time, else the engine's start); the cause when
|
||||
/// the node carries it (its `finality_reason=` / `held_by=` line), else the plain two-thirds line; and the one
|
||||
/// sentence every surface shows: "Finality paused since 18:39 UTC: under two thirds of the weight is signing"
|
||||
pub paused: bool,
|
||||
pub paused_since: f64,
|
||||
pub reason: String,
|
||||
pub held_by: String,
|
||||
pub line: String,
|
||||
}
|
||||
|
||||
/// Clock skew against the network. skew_s = local time minus network time (negative = this machine is behind).
|
||||
|
|
|
|||
|
|
@ -340,6 +340,13 @@ var View = (function () {
|
|||
}
|
||||
|
||||
// the Node page's plain words: n = state.node, clock = state.clock, eta = the sync ETA sentence (may be '')
|
||||
// Horizon polish Q83/Q84 (6 October 2026): while the network's finality is paused every surface shows the engine's
|
||||
// one sentence (f.line, "Finality paused since 18:39 UTC: ...") and no surface claims a lock is final
|
||||
function finalityWords(f) {
|
||||
if (f && f.paused) return { paused: true, age: 'paused', note: f.line || 'Finality paused: under two thirds of the weight is signing' };
|
||||
if (f && f.last_lock > 0) return { paused: false, age: rel(f.age_s), note: 'A lock is a point the miners have agreed can never be undone. Checkpoints lock at 2/3 of the voting weight.' };
|
||||
return { paused: false, age: 'n/a', note: (f && f.message) || 'Locks appear once the miner votes on checkpoints.' };
|
||||
}
|
||||
function nodeWords(n, clock, eta) {
|
||||
var bad = clock && clock.severity === 'block';
|
||||
if (bad) return { word: n.state === 'synced' ? 'synced' : n.state, line: 'Your clock is off by ' + skewWord(clock.skew_s) + '. Mining waits until it is synced.', tone: 'bad' };
|
||||
|
|
@ -413,7 +420,7 @@ var View = (function () {
|
|||
else parts.push('Nothing running' + (j.checked_at ? '; checked ' + rel((now || 0) - j.checked_at) : '') + (j.queued ? '; ' + j.queued + ' queued' : '') + '.');
|
||||
return parts.join(' ');
|
||||
}
|
||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, toggle: toggle, nodeWords: nodeWords, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote };
|
||||
return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, toggle: toggle, nodeWords: nodeWords, finalityWords: finalityWords, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote };
|
||||
})();
|
||||
/* ---------- 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
|
||||
|
|
@ -1122,6 +1129,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
function renderNode(s) {
|
||||
var n = s.node, f = s.finality, eta = syncEta(n, s.now);
|
||||
var w = View.nodeWords(n, s.clock, eta);
|
||||
var fw = View.finalityWords(f);
|
||||
if (fw.paused) w.line = fw.note;
|
||||
setText('d-node', w.word); setText('d-node-sub', w.line);
|
||||
$('d-node-sub').title = w.line + (n.last_reading_age_s >= 0 ? ' · last reading ' + Math.round(n.last_reading_age_s) + ' s ago' : '');
|
||||
var cell = $('n-state-cell'); cell.classList.toggle('ok', w.tone === 'ok'); cell.classList.toggle('bad', w.tone === 'bad');
|
||||
|
|
@ -1143,8 +1152,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
switchesSig = sig;
|
||||
$('n-switches').innerHTML = (n.consensus_switches || []).map(function (x) { return '<span class="' + (sw && x.key === sw.key ? 'next' : x.daa <= n.daa ? 'past' : '') + '"><span>' + esc(x.name) + (x.daa <= n.daa ? ' (applied)' : '') + '</span><span>' + withCommas(x.daa) + '</span></span>'; }).join('');
|
||||
}
|
||||
if (f.last_lock > 0) { setText('f-lock', '#' + withCommas(f.last_lock)); setText('f-age', rel(f.age_s)); setText('f-note', 'A lock is a point the miners have agreed can never be undone. Checkpoints lock at 2/3 of the 30-day weight; this machine votes every 30 s.'); }
|
||||
else { setText('f-lock', 'none yet'); setText('f-age', 'n/a'); setText('f-note', f.message || 'Locks appear once the miner votes on checkpoints.'); }
|
||||
setText('f-lock', f.last_lock > 0 ? '#' + withCommas(f.last_lock) : 'none yet'); setText('f-age', fw.age); setText('f-note', fw.note);
|
||||
$('f-age').classList.toggle('warn', fw.paused);
|
||||
setText('f-votes', withCommas(f.votes));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -198,3 +198,18 @@ test('hot-plug (src/hotplug.rs): a removed card and a faulty card are shown as s
|
|||
const t2 = V.toggle({ state: 'mining', paused: false, cards: [card(), gone] }, { synced: true }, {});
|
||||
assert.equal(t2.sub, 'mining on 1 of 1 card');
|
||||
});
|
||||
|
||||
test('Horizon polish Q83/Q84: while finality is paused every surface shows the one sentence and no lock is called final', () => {
|
||||
const V = mod.exports.View;
|
||||
const paused = V.finalityWords({ paused: true, last_lock: 412, age_s: 1200, line: 'Finality paused since 18:39 UTC: under two thirds of the weight is signing' });
|
||||
assert.equal(paused.paused, true);
|
||||
assert.equal(paused.age, 'paused');
|
||||
assert.equal(paused.note, 'Finality paused since 18:39 UTC: under two thirds of the weight is signing');
|
||||
assert.ok(!/\bfinal\b/i.test(paused.note), 'no "final" word while paused');
|
||||
const locked = V.finalityWords({ paused: false, last_lock: 412, age_s: 90, line: '' });
|
||||
assert.equal(locked.paused, false);
|
||||
assert.ok(locked.note.indexOf('never be undone') > 0);
|
||||
const none = V.finalityWords({ paused: false, last_lock: 0, age_s: 0, message: 'waiting for the miner' });
|
||||
assert.equal(none.age, 'n/a');
|
||||
assert.equal(none.note, 'waiting for the miner');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -326,6 +326,8 @@ For 0.3.16 (main, 6 October 2026 evening), the engine fields Miner UI 4 reads, o
|
|||
| `state.mining.pounds_per_day` | that draw as £ a day at `settings.power_price_pence` (0 when no price) | `ember::pounds_per_day` |
|
||||
| `state.address.balance_wei` | the payout address's balance in wei as a decimal string, `eth_getBalance` through the node's own RPC every 30 s while the node runs (60 s after a failure); null until read; `balance_age_s` (-1 until then), `balance_note` (the last error in words) | engine `tick_balance`, `Cmd::BalanceRead`, `ember::wei_from_hex` + test |
|
||||
| `state.address.price_gbp_per_ign` | null. Its one source will be a SIGNED field of the OTA manifest (`price`: gbp_per_ign, as_of, source), checked like the tuning object; the app never computes or fetches a price itself | state.rs (documented), no code until a market exists |
|
||||
| Horizon polish Q4 (updater) | `fork_is_close`: an activation height at or below the DAA has passed, nothing is pending (the old rule read it as close: every update since the 0.3.14 manifest said "0 blocks away, installing now", stripped Later and skipped every guard; PC 1's 17:52:54Z install under a job came through it); `publish-manifest.sh` refuses an activation height at or below the live DAA (/api/live state.daa) unless `--allow-passed-activation` | manifest.rs + test, packaging/ota/publish-manifest.sh |
|
||||
| Horizon polish Q83/Q84/Q2 (the pause shown) | `state.finality.paused`, `paused_since` (the last lock's time, else the engine's start), `reason`, `held_by`, `line` = "Finality paused since 18:39 UTC: under two thirds of the weight is signing" (the node's cause when it carries one: the engine parses a node log line carrying `finality_reason=<words or "quoted words"> held_by=<id>`, the node lane's to emit); the node line, the Overview's state and the Finality card show the one sentence while paused, the Finality card's age reads "paused", and no surface calls a lock final; `finality.message` carries the sentence too so older UIs show it | ember.rs `finality_paused_line`, `parse_finality_line`, `hhmm_utc` + tests; engine.rs derive and the node-line parse; ui/app.js `View.finalityWords` + test |
|
||||
| the finality rule (updater) | Horizon frontier lane: the updater installs nothing while the network's finality is paused (a synced node with no checkpoint lock for `manifest::FINALITY_PAUSE_S` = 15 min; the last LOCK line's age, else the engine's uptime); the update card reads "waiting for finality: ..."; slot, catch-up and patience rules unchanged otherwise; only the signed manifest's own `urgent` flag installs through a pause (a fork-close or unsupported urgency does not); the known-failed case is the test | manifest.rs `Moment.finality_paused`, `manifest_urgent`, `Manifest.urgent`, `safe_to_apply` + test; ota.rs `Ctx`; engine.rs |
|
||||
| `GET /api/live` | the observer's reply shape from a local source: `"source": "node"` when igneumd carries `igneum_getRecentBlocks(seconds)` (the node lane, a283f5f0d364ceef0; the engine computes miners_10m, blocks_10m, blocks_per_minute, the 90 s blocks and the miners list from it and takes the DAG numbers from its node state), else `"source": "site"` (the public reply, fetched by curl, cached 60 s) with `age_s`; `pending: true` before the first fetch | src/live.rs (`shape_from_blocks`, `parse_recent` + tests), server.rs |
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
# packaging/ota/publish-manifest.sh --version 0.3.1 --mac packaging/mac/dist/Igneum-Miner-0.3.1.dmg \
|
||||
# [--win packaging/windows/dist/Igneum-Miner-Setup-0.3.1.exe] --notes "one line of what changed" \
|
||||
# [--activation-height 120000 --deadline-note "difficulty v2"] [--min-supported 0.3.0] [--channel devnet] [--deploy]
|
||||
# [--allow-passed-activation] (Horizon polish Q4: an activation height at or below the live DAA is refused otherwise)
|
||||
# [--override '{"difficulty_v2_activation_daa":33000,"proving_v0_activation_daa":120000}']
|
||||
# consensus.override: the exact object every app writes to its override.json (the node's
|
||||
# --override-params-file), so it carries EVERY height switch, not just the new one;
|
||||
|
|
@ -47,6 +48,7 @@ while [ $# -gt 0 ]; do
|
|||
--win) WIN="$2"; shift 2 ;;
|
||||
--notes) NOTES="$2"; shift 2 ;;
|
||||
--activation-height) ACTIVATION="$2"; shift 2 ;;
|
||||
--allow-passed-activation) ALLOW_PASSED=1; shift ;;
|
||||
--deadline-note) DEADLINE="$2"; shift 2 ;;
|
||||
--override) OVERRIDE="$2"; shift 2 ;; # consensus.override: the exact JSON object every app writes to its override.json (all height switches, not just the new one)
|
||||
--min-supported) MIN_SUPPORTED="$2"; shift 2 ;;
|
||||
|
|
@ -173,6 +175,23 @@ fi
|
|||
|
||||
# canonical JSON: sorted keys, no whitespace; the signature is over these exact bytes
|
||||
NEW="$DEST/igneum-app-latest.json.new"
|
||||
# Horizon polish Q4 (6 October 2026): an activation height that has already passed makes every app read the fork as
|
||||
# close (0.3.14's manifest carried difficulty v2 at 33,000 against a DAA over 200,000: "0 blocks away, installing
|
||||
# now" on every update, every safe-moment guard skipped). The live DAA comes from the public /api/live; a height at or
|
||||
# below it is refused unless --allow-passed-activation says so.
|
||||
if [ -n "$ACTIVATION" ]; then
|
||||
LIVE_DAA="$(curl -fsS --max-time 10 "${IGNEUM_LIVE_API:-https://igneum.network/api/live}" 2>/dev/null | python3 -c 'import json,sys; print(int((json.load(sys.stdin).get("state") or {}).get("daa") or 0))' 2>/dev/null || echo 0)"
|
||||
if [ "${LIVE_DAA:-0}" -gt 0 ] && [ "$ACTIVATION" -le "$LIVE_DAA" ]; then
|
||||
if [ "${ALLOW_PASSED:-0}" = 1 ]; then
|
||||
echo "activation height $ACTIVATION is at or below the live DAA $LIVE_DAA (passed); published anyway on --allow-passed-activation" >&2
|
||||
else
|
||||
echo "refused: activation height $ACTIVATION is at or below the live DAA $LIVE_DAA, so it has passed; a passed activation makes every app read the fork as close (0.3.14 manifest). Drop --activation-height or pass --allow-passed-activation" >&2
|
||||
exit 2
|
||||
fi
|
||||
elif [ "${LIVE_DAA:-0}" -eq 0 ]; then
|
||||
echo "note: the live DAA could not be read from /api/live; the activation height $ACTIVATION is not checked against it" >&2
|
||||
fi
|
||||
fi
|
||||
python3 - "$NEW" "$VERSION" "$CHANNEL" "$NOTES" "$MIN_SUPPORTED" "$ACTIVATION" "$DEADLINE" "$MAC_ENTRY" "$WIN_ENTRY" "${OVERRIDE:-}" "${TUNING:-}" <<'PY'
|
||||
import json, sys, datetime
|
||||
out, version, channel, notes, min_supported, activation, deadline, mac, win, override, tuning = sys.argv[1:12]
|
||||
|
|
|
|||
Loading…
Reference in a new issue