Fix BTC-vs-source-IP confusion; redesign user page; OSS + TLS icons
ckpool's stratum_instance.address (exposed as the "address" field in
the runtime JSON) is the SOURCE IP of the connection — set from
inet_ntop in connector.c — not the BTC payout address. The miner's
BTC payout comes from the stratum username, which ckpool stores in
worker.user (and in the dotted prefix of worker.workername).
Every "user" reference in the dashboard was reading c.address and
treating it as the BTC. Effects:
• clicking an online miner navigated to #/user/<source-ip>; the
UserDetailPage filters never matched and the page rendered with
junk values (or the user struct's stale residual hashrate when
the BTC happened to come from a sibling row).
• offline-miner clicks worked, but totals came from user.dsps*
which decay slowly inside ckpool, so a miner that had just
disconnected still showed positive hashrate for several minutes.
• the "Best (ever)" tile fell back to bestdiff (session) when
bestever was zero, so it lied about its semantics.
Fixes:
• Add btcAddressOf() helper and use w.user (or it) when extracting
the BTC for the user-link button. The source IP gets its own
sub-line under the worker name, clearly labelled.
• Redesign UserDetailPage: filter clients by workername prefix
against the BTC, never by c.address; compute hashrate totals by
SUMMING the user's currently-connected clients (so 0 online
clients => 0 hashrate, no stale decay artifacts); compute
best_ever as max across the user's worker.bestever values; show
online/total worker counts and a per-worker status pill.
• Add an explorer link (mempool.space) for the user's BTC.
TLS detection moves to a clean signal: ckpool now binds two stratum
sockets — public plaintext and loopback-only. stunnel forwards to
the loopback bind, so TLS clients arrive with c.server == 1. The
dashboard reads that and renders a green TLS pill next to the
worker name. No source-IP heuristics needed.
Open-source mark: new isOpenSource() heuristic over the stratum
useragent matches Bitaxe family (NerdAxe / NerdQAxe / NerdMiner /
NerdOctaxe / Lucky / QAxe / MCCM), Braiins OS, cgminer / bfgminer /
ckminer, and ESP32 builds. Renders as an orange ★ next to the
hardware label, matching public-pool's convention.
types.ts: document StratumClient.address (source IP, not BTC) and
add the previously-undeclared `server` field. Surfacing the runtime
value that has been there all along since cfb0f83.
This commit is contained in:
@@ -1,27 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { snap } from "../stores/snapshot.svelte";
|
||||
import { selectUser } from "../stores/selection.svelte";
|
||||
import { formatHashrate, formatDifficulty, formatAgo, detectHardware } from "../format";
|
||||
import {
|
||||
formatHashrate,
|
||||
formatDifficulty,
|
||||
formatAgo,
|
||||
detectHardware,
|
||||
isOpenSource,
|
||||
btcAddressOf,
|
||||
} from "../format";
|
||||
import type { StratumClient, Worker } from "../types";
|
||||
|
||||
// Join StratumClient (live session data: useragent, IP, diff) with
|
||||
// Worker (best-share data). Clients without a matching worker still
|
||||
// render; workers without an active client are dimmed.
|
||||
type Row = {
|
||||
workerName: string;
|
||||
user: string;
|
||||
btcAddress: string;
|
||||
sourceIp: string;
|
||||
hardware: string;
|
||||
openSource: boolean;
|
||||
tls: boolean;
|
||||
hashrate1m: number;
|
||||
hashrate1h: number;
|
||||
bestDiff: number;
|
||||
bestSession: number;
|
||||
bestEver: number;
|
||||
lastShare: number;
|
||||
difficulty: number;
|
||||
address: string;
|
||||
idle: boolean;
|
||||
online: boolean;
|
||||
};
|
||||
|
||||
// The "address" field on a ckpool stratum_instance is the SOURCE IP
|
||||
// the connection arrived on (see connector.c:311 — it's set with
|
||||
// inet_ntop). The miner's BTC payout address comes from the worker
|
||||
// name (which is the stratum username, format <btc>[.label]) or
|
||||
// from the joined worker_instance, which ckpool keys by full
|
||||
// workername with user = the BTC.
|
||||
//
|
||||
// ckpool's serverurl array gives us a second piece of info: TLS
|
||||
// traffic comes in via stunnel on the loopback-only bind (server
|
||||
// index 1), so client.server === 1 means this miner is using TLS.
|
||||
const rows = $derived.by<Row[]>(() => {
|
||||
const clients = snap.data?.clients ?? [];
|
||||
const workers = snap.data?.workers ?? [];
|
||||
@@ -35,23 +51,20 @@
|
||||
const wname = c.workername || `${c.address}.unnamed`;
|
||||
seen.add(wname);
|
||||
const w = byWorker.get(wname);
|
||||
// "Session best" = best diff for *this stratum TCP session*.
|
||||
// ckpool zeroes stratum_instance.best_diff when the connection
|
||||
// ends and on pool restart (in-memory only), so this is the
|
||||
// value that should reset on miner disconnect / pool restart.
|
||||
// Don't fall back to worker.best_diff — that's the persistent
|
||||
// round counter and would defeat the reset semantics.
|
||||
const btcAddress = w?.user ?? btcAddressOf(wname);
|
||||
out.push({
|
||||
workerName: wname,
|
||||
user: c.address,
|
||||
btcAddress,
|
||||
sourceIp: c.address,
|
||||
hardware: detectHardware(c.useragent),
|
||||
openSource: isOpenSource(c.useragent),
|
||||
tls: c.server === 1,
|
||||
hashrate1m: c.dsps1 * 2 ** 32,
|
||||
hashrate1h: c.dsps60 * 2 ** 32,
|
||||
bestDiff: c.bestdiff,
|
||||
bestEver: w?.bestever ?? c.bestdiff,
|
||||
bestSession: c.bestdiff,
|
||||
bestEver: w?.bestever ?? 0,
|
||||
lastShare: c.lastshare,
|
||||
difficulty: c.diff,
|
||||
address: c.address,
|
||||
idle: c.idle,
|
||||
online: true,
|
||||
});
|
||||
@@ -59,19 +72,19 @@
|
||||
|
||||
for (const w of workers) {
|
||||
if (seen.has(w.worker)) continue;
|
||||
// Offline worker — no current session, so session-best is 0.
|
||||
// The lifetime best_ever still applies.
|
||||
out.push({
|
||||
workerName: w.worker,
|
||||
user: w.user,
|
||||
btcAddress: w.user,
|
||||
sourceIp: "",
|
||||
hardware: "offline",
|
||||
hashrate1m: w.dsps1 * 2 ** 32,
|
||||
hashrate1h: w.dsps60 * 2 ** 32,
|
||||
bestDiff: 0,
|
||||
openSource: false,
|
||||
tls: false,
|
||||
hashrate1m: 0,
|
||||
hashrate1h: 0,
|
||||
bestSession: 0,
|
||||
bestEver: w.bestever,
|
||||
lastShare: w.lastshare,
|
||||
difficulty: w.mindiff,
|
||||
address: w.user,
|
||||
idle: w.idle,
|
||||
online: false,
|
||||
});
|
||||
@@ -105,19 +118,32 @@
|
||||
{#each rows as r (r.workerName)}
|
||||
<tr class:offline={!r.online} class:idle={r.idle}>
|
||||
<td>
|
||||
<div class="wname mono">{r.workerName}</div>
|
||||
<div class="wname mono">
|
||||
{r.workerName}
|
||||
{#if r.tls}
|
||||
<span class="badge tls" title="Connected via TLS">TLS</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="addr-btn mono"
|
||||
onclick={() => selectUser(r.address)}
|
||||
onclick={() => selectUser(r.btcAddress)}
|
||||
title="View per-user stats"
|
||||
>{r.address}</button>
|
||||
>{r.btcAddress}</button>
|
||||
{#if r.sourceIp}
|
||||
<div class="src-ip mono" title="Source IP">{r.sourceIp}</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{r.hardware}
|
||||
{#if r.openSource}
|
||||
<span class="oss" title="Open source hardware or firmware">★</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{r.hardware}</td>
|
||||
<td class="num">{formatHashrate(r.hashrate1m)}</td>
|
||||
<td class="num">{formatHashrate(r.hashrate1h)}</td>
|
||||
<td class="num">{formatDifficulty(r.difficulty)}</td>
|
||||
<td class="num">{formatDifficulty(r.bestDiff)}</td>
|
||||
<td class="num">{formatDifficulty(r.bestSession)}</td>
|
||||
<td class="num">{formatDifficulty(r.bestEver)}</td>
|
||||
<td class="num">{formatAgo(r.lastShare)}</td>
|
||||
</tr>
|
||||
@@ -143,6 +169,10 @@
|
||||
}
|
||||
.wname {
|
||||
font-size: 0.95em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.addr-btn {
|
||||
display: inline-block;
|
||||
@@ -165,6 +195,30 @@
|
||||
background: transparent;
|
||||
outline: none;
|
||||
}
|
||||
.src-ip {
|
||||
color: var(--fg-dim);
|
||||
font-size: 0.72em;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
.badge.tls {
|
||||
display: inline-block;
|
||||
font-size: 0.62em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--good);
|
||||
border: 1px solid #2f5c48;
|
||||
background: #0f2419;
|
||||
padding: 0.05em 0.45em;
|
||||
border-radius: 999px;
|
||||
text-transform: none;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
.oss {
|
||||
color: var(--accent);
|
||||
font-size: 0.95em;
|
||||
margin-left: 0.3em;
|
||||
cursor: help;
|
||||
}
|
||||
tr.offline {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user