Add node peers, blue banners, glowing diff ranges, fix back navigation
- Show Bitcoin Core peer count on block height card (hover for in/out) - BestSharePage disclaimers now use solid blue styling - Difficulty distribution ranges glow from warm peach to deep red - Back-to-dashboard buttons always navigate to dashboard instead of using browser history
This commit is contained in:
@@ -343,6 +343,20 @@ func (c *RPC) GetRawMempoolVerbose(ctx context.Context) (map[string]RawMempoolEn
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NetworkInfo struct {
|
||||||
|
Connections int `json:"connections"`
|
||||||
|
ConnectionsIn int `json:"connections_in"`
|
||||||
|
ConnectionsOut int `json:"connections_out"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RPC) GetNetworkInfo(ctx context.Context) (*NetworkInfo, error) {
|
||||||
|
var out NetworkInfo
|
||||||
|
if err := c.Call(ctx, "getnetworkinfo", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsRPCError checks if err is an rpcError with a specific code.
|
// IsRPCError checks if err is an rpcError with a specific code.
|
||||||
func IsRPCError(err error, code int) bool {
|
func IsRPCError(err error, code int) bool {
|
||||||
var rerr *rpcError
|
var rerr *rpcError
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ type Snapshot struct {
|
|||||||
HasLastZMQEvent bool `json:"has_last_zmq_event"`
|
HasLastZMQEvent bool `json:"has_last_zmq_event"`
|
||||||
TipChangedAge float64 `json:"tip_changed_age"` // seconds since tip height last changed
|
TipChangedAge float64 `json:"tip_changed_age"` // seconds since tip height last changed
|
||||||
|
|
||||||
|
// Bitcoin Core peer connections.
|
||||||
|
PeerCount int `json:"peer_count"`
|
||||||
|
PeersIn int `json:"peers_in"`
|
||||||
|
PeersOut int `json:"peers_out"`
|
||||||
|
|
||||||
// Share counters: raw counts (1 submission = 1 share regardless of diff).
|
// Share counters: raw counts (1 submission = 1 share regardless of diff).
|
||||||
// Session = since ckpool started; AllTime = persisted across restarts.
|
// Session = since ckpool started; AllTime = persisted across restarts.
|
||||||
SessionAccepted int64 `json:"session_accepted"`
|
SessionAccepted int64 `json:"session_accepted"`
|
||||||
@@ -460,6 +465,13 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- bitcoind: peer connections ---
|
||||||
|
if ni, err := a.RPC.GetNetworkInfo(ctx); err == nil {
|
||||||
|
next.PeerCount = ni.Connections
|
||||||
|
next.PeersIn = ni.ConnectionsIn
|
||||||
|
next.PeersOut = ni.ConnectionsOut
|
||||||
|
}
|
||||||
|
|
||||||
// --- predicted difficulty adjustment ---
|
// --- predicted difficulty adjustment ---
|
||||||
// Fetch the timestamp of the first block in the current retarget
|
// Fetch the timestamp of the first block in the current retarget
|
||||||
// epoch (height - height%2016) once per epoch and cache it, then
|
// epoch (height - height%2016) once per epoch and cache it, then
|
||||||
|
|||||||
@@ -399,15 +399,15 @@
|
|||||||
}
|
}
|
||||||
.disclaimer {
|
.disclaimer {
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
color: var(--fg-dim);
|
color: rgb(160, 195, 240);
|
||||||
background: rgba(245, 196, 71, 0.08);
|
background: rgb(20, 35, 60);
|
||||||
border: 1px solid rgba(245, 196, 71, 0.25);
|
border: 1px solid rgb(60, 100, 170);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.5em 0.85em;
|
padding: 0.5em 0.85em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
.disclaimer strong {
|
.disclaimer strong {
|
||||||
color: var(--fg);
|
color: rgb(200, 220, 250);
|
||||||
}
|
}
|
||||||
.head {
|
.head {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -208,7 +208,12 @@
|
|||||||
<div class="card height-card" class:new-block={heightFlash}>
|
<div class="card height-card" class:new-block={heightFlash}>
|
||||||
<div class="stat-label">Block height</div>
|
<div class="stat-label">Block height</div>
|
||||||
<div class="stat-value">{height ? height.toLocaleString() : "—"}</div>
|
<div class="stat-value">{height ? height.toLocaleString() : "—"}</div>
|
||||||
<div class="stat-sub">{data.chain?.chain === "main" ? "mainnet" : data.chain?.chain ?? "—"}</div>
|
<div class="stat-sub">
|
||||||
|
{data.chain?.chain === "main" ? "mainnet" : data.chain?.chain ?? "—"}
|
||||||
|
{#if data.peer_count > 0}
|
||||||
|
<span class="peers" title="{data.peers_in} inbound · {data.peers_out} outbound"> · {data.peer_count} peers</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
|
|
||||||
// --- Difficulty distribution ---
|
// --- Difficulty distribution ---
|
||||||
const bucketLabels = ["< 1M", "1M\u2013100M", "100M\u20131G", "1G\u2013100G", "100G\u20131T", "\u2265 1T"];
|
const bucketLabels = ["< 1M", "1M\u2013100M", "100M\u20131G", "1G\u2013100G", "100G\u20131T", "\u2265 1T"];
|
||||||
|
// Fire intensity classes: none for <1M, then progressively more intense.
|
||||||
|
const fireClasses = ["", "fire-1", "fire-2", "fire-3", "fire-4", "fire-5"];
|
||||||
|
|
||||||
type BucketRow = {
|
type BucketRow = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -180,7 +182,7 @@
|
|||||||
|
|
||||||
<!-- Difficulty Distribution Table -->
|
<!-- Difficulty Distribution Table -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h3>Difficulty Distribution</h3>
|
<h3>Difficulty Distribution - Amount of shares in different difficulty ranges</h3>
|
||||||
{#if !hasDistData}
|
{#if !hasDistData}
|
||||||
<div class="empty">No accepted shares yet</div>
|
<div class="empty">No accepted shares yet</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -196,9 +198,16 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each bucketRows as row}
|
{#each bucketRows as row, i}
|
||||||
<tr class:dim-row={row.session === 0 && row.alltime === 0}>
|
{@const hasShares = row.session > 0 || row.alltime > 0}
|
||||||
<td>{row.label}</td>
|
<tr class:dim-row={!hasShares && !fireClasses[i]}>
|
||||||
|
<td>
|
||||||
|
{#if fireClasses[i]}
|
||||||
|
<span class="fire-label {fireClasses[i]}" class:fire-active={hasShares} class:fire-dimmed={!hasShares}>{row.label}</span>
|
||||||
|
{:else}
|
||||||
|
{row.label}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
<td class="num">{row.session.toLocaleString()}</td>
|
<td class="num">{row.session.toLocaleString()}</td>
|
||||||
<td class="num">{row.sessionPct.toFixed(1)}%</td>
|
<td class="num">{row.sessionPct.toFixed(1)}%</td>
|
||||||
<td class="num">{row.alltime.toLocaleString()}</td>
|
<td class="num">{row.alltime.toLocaleString()}</td>
|
||||||
@@ -310,4 +319,24 @@
|
|||||||
tr[title]:hover td { color: var(--accent); }
|
tr[title]:hover td { color: var(--accent); }
|
||||||
.dim-row td { opacity: 0.35; }
|
.dim-row td { opacity: 0.35; }
|
||||||
|
|
||||||
|
/* --- Glowing range labels --- */
|
||||||
|
.fire-label {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.fire-dimmed { opacity: 0.2; }
|
||||||
|
.fire-active { animation: glow-pulse 2s ease-in-out infinite alternate; }
|
||||||
|
|
||||||
|
.fire-1.fire-active { color: #ffcc80; --glow: 255, 180, 100; }
|
||||||
|
.fire-2.fire-active { color: #ff9a5c; --glow: 255, 140, 60; }
|
||||||
|
.fire-3.fire-active { color: #ff6e40; --glow: 255, 90, 30; }
|
||||||
|
.fire-4.fire-active { color: #f44336; --glow: 230, 40, 20; }
|
||||||
|
.fire-5.fire-active { color: #c62828; --glow: 180, 20, 10; }
|
||||||
|
|
||||||
|
@keyframes glow-pulse {
|
||||||
|
0% { text-shadow: 0 0 8px rgba(var(--glow), 0.4); }
|
||||||
|
100% { text-shadow: 0 0 8px rgba(var(--glow), 0.85),
|
||||||
|
0 0 16px rgba(var(--glow), 0.3); }
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -74,16 +74,5 @@ export function selectBestShare(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clearSelection(): void {
|
export function clearSelection(): void {
|
||||||
if (
|
window.location.hash = "";
|
||||||
window.history.length > 1 &&
|
|
||||||
(window.location.hash.startsWith(USER_PREFIX) ||
|
|
||||||
window.location.hash.startsWith(WORKER_PREFIX) ||
|
|
||||||
window.location.hash === ACCELERATOR_HASH ||
|
|
||||||
window.location.hash === STATS_HASH ||
|
|
||||||
window.location.hash === BESTSHARE_HASH)
|
|
||||||
) {
|
|
||||||
window.history.back();
|
|
||||||
} else {
|
|
||||||
window.location.hash = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ export type Snapshot = {
|
|||||||
has_last_zmq_event: boolean;
|
has_last_zmq_event: boolean;
|
||||||
last_zmq_event_age?: number; // seconds
|
last_zmq_event_age?: number; // seconds
|
||||||
tip_changed_age: number; // seconds since tip height last changed
|
tip_changed_age: number; // seconds since tip height last changed
|
||||||
|
peer_count: number;
|
||||||
|
peers_in: number;
|
||||||
|
peers_out: number;
|
||||||
// Share counters (raw, 1 submission = 1 share).
|
// Share counters (raw, 1 submission = 1 share).
|
||||||
session_accepted: number;
|
session_accepted: number;
|
||||||
session_rejected: number;
|
session_rejected: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user