diff --git a/api/internal/state/aggregator.go b/api/internal/state/aggregator.go index f4d0b04..b719c8d 100644 --- a/api/internal/state/aggregator.go +++ b/api/internal/state/aggregator.go @@ -53,6 +53,11 @@ type Snapshot struct { // in BTC. Refreshed at most once per minute. NextBlockRewardBTC float64 `json:"next_block_reward_btc"` + // Predicted percent change at the next difficulty retarget, clamped + // to Bitcoin's consensus range of [-75, +300]. Derived from the + // observed block interval since the current epoch started. + NextDifficultyPercent float64 `json:"next_difficulty_percent"` + // Bitcoin Core fields Chain *bitcoind.BlockchainInfo `json:"chain"` NetworkHashrateHs float64 `json:"network_hashrate_hs"` @@ -115,6 +120,12 @@ type Aggregator struct { nextBlockReward float64 lastTemplateFetch time.Time + // Retarget epoch-start timestamp cache. Updated when we cross a + // new retarget boundary (every 2016 blocks); within an epoch we + // just re-use the cached value and re-extrapolate each refresh. + retargetEpoch int64 + retargetStartUnix int64 + // ckFailStreak counts consecutive refreshes where CKPool returned an // error. The first failure after a streak of successes is logged at // DEBUG (likely a transient warm-up or lock hiccup); repeated failures @@ -220,6 +231,45 @@ func (a *Aggregator) refresh(ctx context.Context) { } } + // --- predicted difficulty adjustment --- + // Fetch the timestamp of the first block in the current retarget + // epoch (height - height%2016) once per epoch and cache it, then + // compute (expected / elapsed - 1) * 100 where expected = blocks_in * + // 600 seconds. Bitcoin clamps the consensus adjustment factor to + // [1/4, 4x], i.e. [-75%, +300%]. + if next.Chain != nil && next.Chain.Blocks > 0 { + height := next.Chain.Blocks + epoch := height / 2016 + inEpoch := height % 2016 + if inEpoch > 0 { + if a.retargetEpoch != epoch || a.retargetStartUnix == 0 { + startHeight := epoch * 2016 + lookupCtx, cancelLookup := context.WithTimeout(ctx, 3*time.Second) + if hash, err := a.RPC.GetBlockHash(lookupCtx, startHeight); err == nil { + if blk, err := a.RPC.GetBlock(lookupCtx, hash); err == nil { + a.retargetEpoch = epoch + a.retargetStartUnix = blk.Time + } + } + cancelLookup() + } + if a.retargetStartUnix > 0 { + elapsed := float64(time.Now().Unix() - a.retargetStartUnix) + expected := float64(inEpoch) * 600 + if elapsed > 0 { + factor := expected / elapsed + if factor > 4 { + factor = 4 + } + if factor < 0.25 { + factor = 0.25 + } + next.NextDifficultyPercent = (factor - 1) * 100 + } + } + } + } + // Compute pool-wide best-ever share diff from workers. for _, w := range next.Workers { d := w.BestEver diff --git a/ui/src/app.css b/ui/src/app.css index 141a96d..8b7b886 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -27,13 +27,26 @@ html, body { margin: 0; padding: 0; - background: var(--bg); color: var(--fg); min-height: 100vh; } +/* Kamado-inspired backdrop: a warm ember glow at the top (the Hinokami + * Kagura / hearth flame) layered over a very low-opacity checker + * pattern nodding to Tanjiro's haori. Fixed to the viewport so it + * doesn't scroll away from the content. */ body { font-variant-numeric: tabular-nums; + background-color: var(--bg); + background-image: + radial-gradient(ellipse 1400px 820px at 50% -120px, rgba(255, 100, 40, 0.22), transparent 62%), + radial-gradient(circle 420px at 8% 14%, rgba(180, 40, 20, 0.10), transparent 70%), + radial-gradient(circle 520px at 92% 22%, rgba(30, 90, 60, 0.07), transparent 70%), + url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat, no-repeat, no-repeat, repeat; + background-size: 100% auto, auto, auto, 96px 96px; + background-position: 0 0, 0 0, 0 0, 0 0; + background-attachment: fixed, fixed, fixed, fixed; } a { @@ -65,10 +78,12 @@ code, } .card { - background: var(--bg-card); + background: rgba(21, 26, 36, 0.88); border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem 1.5rem; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); } .grid { @@ -85,20 +100,22 @@ code, .stat-label { color: var(--fg-dim); - font-size: 0.85em; + font-size: 0.82em; text-transform: uppercase; - letter-spacing: 0.04em; - margin-bottom: 0.35em; + letter-spacing: 0.05em; + margin-bottom: 0.4em; } .stat-value { font-size: 1.7rem; font-weight: 600; letter-spacing: -0.01em; + line-height: 1.15; } .stat-sub { color: var(--fg-dim); - font-size: 0.85em; - margin-top: 0.25em; + font-size: 0.95em; + margin-top: 0.4em; + line-height: 1.4; } table { diff --git a/ui/src/lib/Header.svelte b/ui/src/lib/Header.svelte index 53745e2..b600b71 100644 --- a/ui/src/lib/Header.svelte +++ b/ui/src/lib/Header.svelte @@ -1,5 +1,4 @@ @@ -31,12 +21,9 @@ 🔥 Kamado Pool -