Persist best share glow, share animations, and UI polish

Backend:
- Persist best_diff as a high-water mark in kv store so it survives
  restarts and transient ckpool gaps
- Persist acked_best_diff so the "new best share" glow survives pool
  restarts and works across multiple new bests before dismissal
- Add POST /api/admin/ack-best and /api/admin/reset-ack-best endpoints

Frontend:
- Best share card: golden glow, shimmer, sparkles, "NEW BEST SHARE!"
  tag with previous value, "Nice" dismiss button
- Optimistic local override so Nice/test buttons respond instantly
  instead of waiting for next WebSocket push
- Fix shimmer sweep to go left-to-right (not start from middle)
- Share animations: reject (red flash + shake) suppresses accept;
  reject effect declared first so guard reads correct state
- Share tooltips show exact counts on hover
- Block reward tooltip shows full amount in satoshis
- Expected block uses 1h hashrate instead of 1m
- formatDuration uses full unit names; years always show 1 decimal
- formatWork always shows 2 decimal places
- Map chain "main" to "mainnet" in block height card
This commit is contained in:
satoshi
2026-05-12 21:56:02 +03:00
parent 467fe5e2ef
commit f1dc0a8a79
6 changed files with 392 additions and 30 deletions
+12
View File
@@ -41,6 +41,8 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /api/ws", s.handleWS)
mux.HandleFunc("GET /api/admin/debug-blocks", s.debugBlocks)
mux.HandleFunc("POST /api/admin/reset-latency", s.resetLatency)
mux.HandleFunc("POST /api/admin/ack-best", s.ackBest)
mux.HandleFunc("POST /api/admin/reset-ack-best", s.resetAckBest)
mux.HandleFunc("POST /api/accelerate", s.accelerate)
mux.HandleFunc("POST /api/accelerate/cancel", s.accelerateCancel)
mux.HandleFunc("POST /api/accelerate/max", s.accelerateMax)
@@ -220,3 +222,13 @@ func (s *Server) resetLatency(w http.ResponseWriter, _ *http.Request) {
s.Agg.ResetLatency()
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
func (s *Server) ackBest(w http.ResponseWriter, _ *http.Request) {
s.Agg.AckBestDiff()
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
func (s *Server) resetAckBest(w http.ResponseWriter, _ *http.Request) {
s.Agg.ResetAckedBestDiff()
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
+70 -1
View File
@@ -43,6 +43,9 @@ type Snapshot struct {
// Best share difficulty ever seen across all workers.
BestDiff float64 `json:"best_diff"`
// Last best-diff value acknowledged by the user via the UI.
// The frontend shows a "new best" glow when best_diff > this.
AckedBestDiff float64 `json:"acked_best_diff"`
// Cumulative work done by the pool across its entire lifetime, in
// diff-1-normalized shares (multiply by 2^32 for total hashes).
@@ -126,6 +129,9 @@ const (
kvLatencyLastMs = "latency_last_ms"
kvStaleWorkHashes = "stale_work_hashes"
kvAckedBestDiff = "acked_best_diff"
kvBestDiff = "best_diff"
kvAllTimeAccepted = "alltime_accepted"
kvAllTimeRejected = "alltime_rejected"
@@ -236,6 +242,13 @@ type Aggregator struct {
latencySumMs int64 // sum of all latencies in ms
latencyLastMs int64 // most recent observation
staleWorkHashes float64 // cumulative wasted hashes = sum(latency_s * hashrate_at_event)
// High-water mark for the best share difficulty ever seen.
// Persisted to kv so it survives restarts and transient ckpool gaps.
bestDiff float64
// Last best-diff value acknowledged by the user in the UI.
ackedBestDiff float64
}
func New(ck *ckpool.Client, rpc *bitcoind.RPC, interval time.Duration, log *slog.Logger) *Aggregator {
@@ -406,7 +419,9 @@ func (a *Aggregator) refresh(ctx context.Context) {
}
}
// Compute pool-wide best-ever share diff from workers.
// Compute pool-wide best-ever share diff from workers, then clamp
// to the persisted high-water mark so a ckpool restart or transient
// socket timeout can never lower the value.
for _, w := range next.Workers {
d := w.BestEver
if d == 0 {
@@ -418,6 +433,17 @@ func (a *Aggregator) refresh(ctx context.Context) {
}
a.mu.Lock()
// High-water mark: only increases, never decreases.
if next.BestDiff > a.bestDiff {
a.bestDiff = next.BestDiff
if a.Store != nil {
val := strconv.FormatFloat(a.bestDiff, 'f', -1, 64)
_ = a.Store.SetKV(kvBestDiff, val)
}
}
next.BestDiff = a.bestDiff
next.AckedBestDiff = a.ackedBestDiff
now := time.Now()
// --- cumulative work tracking ---
@@ -554,6 +580,31 @@ func (a *Aggregator) refresh(ctx context.Context) {
a.markReady()
}
// AckBestDiff records the current best_diff as acknowledged so the UI
// stops showing the "new best share" glow until a higher one appears.
func (a *Aggregator) AckBestDiff() {
a.mu.Lock()
a.ackedBestDiff = a.snap.BestDiff
a.mu.Unlock()
if a.Store != nil {
val := strconv.FormatFloat(a.snap.BestDiff, 'f', -1, 64)
if err := a.Store.SetKV(kvAckedBestDiff, val); err != nil {
a.Log.Warn("acked_best_diff persist failed", "err", err)
}
}
}
// ResetAckedBestDiff clears the acknowledged best diff so the "new best
// share" glow reappears. Used by the debug/test UI button.
func (a *Aggregator) ResetAckedBestDiff() {
a.mu.Lock()
a.ackedBestDiff = 0
a.mu.Unlock()
if a.Store != nil {
_ = a.Store.SetKV(kvAckedBestDiff, "0")
}
}
// loadPersistedState restores cumulative work and hashrate history from
// the store so they survive process restarts. Safe to call with a nil
// Store — becomes a no-op.
@@ -604,6 +655,24 @@ func (a *Aggregator) loadPersistedState() {
}
}
// Restore best diff high-water mark.
if v, err := a.Store.GetKV(kvBestDiff); err == nil && v != "" {
if f, perr := strconv.ParseFloat(v, 64); perr == nil {
a.mu.Lock()
a.bestDiff = f
a.mu.Unlock()
}
}
// Restore acknowledged best diff.
if v, err := a.Store.GetKV(kvAckedBestDiff); err == nil && v != "" {
if f, perr := strconv.ParseFloat(v, 64); perr == nil {
a.mu.Lock()
a.ackedBestDiff = f
a.mu.Unlock()
}
}
// Restore latency stats.
a.mu.Lock()
if v, err := a.Store.GetKV(kvLatencyCount); err == nil && v != "" {