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:
@@ -12,7 +12,8 @@
|
||||
"blockpoll": ${BLOCKPOLL_MS},
|
||||
"update_interval": ${UPDATE_INTERVAL_S},
|
||||
"serverurl": [
|
||||
"0.0.0.0:${STRATUM_PORT}"
|
||||
"0.0.0.0:${STRATUM_PORT}",
|
||||
"127.0.0.1:${TLS_INTERNAL_PORT}"
|
||||
],
|
||||
"mindiff": ${MINDIFF},
|
||||
"startdiff": ${STARTDIFF},
|
||||
|
||||
@@ -63,6 +63,25 @@ export function formatAgo(unixSeconds: number): string {
|
||||
return `${Math.floor(ageSec / 86400)}d ago`;
|
||||
}
|
||||
|
||||
// Open-source ASIC hardware (Bitaxe family, NerdMiner, etc.) and
|
||||
// open-source firmware projects (Braiins OS, cgminer/bfgminer/
|
||||
// ckminer). Used to render a star next to the hardware label, like
|
||||
// public-pool does, so visitors can spot DIY / OSS rigs at a glance.
|
||||
export function isOpenSource(ua: string): boolean {
|
||||
if (!ua) return false;
|
||||
const s = ua.toLowerCase();
|
||||
return /bitaxe|nerdaxe|nerdqaxe|nerdoctaxe|nerdminer|qaxe|mccm|lucky|braiins|cgminer|bfgminer|ckminer|esp32/.test(s);
|
||||
}
|
||||
|
||||
// Extract the BTC payout address from a stratum username. Stratum
|
||||
// usernames in solo mode are "<btc-address>" or "<btc-address>.label".
|
||||
// ckpool stores this verbatim in the worker name.
|
||||
export function btcAddressOf(workername: string): string {
|
||||
if (!workername) return "";
|
||||
const dot = workername.indexOf(".");
|
||||
return dot < 0 ? workername : workername.slice(0, dot);
|
||||
}
|
||||
|
||||
// Detect common Bitcoin mining hardware from the stratum useragent
|
||||
// string. This is a best-effort heuristic; unknown agents fall back
|
||||
// to a stripped version of the raw string. Order matters — check
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,69 +6,90 @@
|
||||
formatDifficulty,
|
||||
formatAgo,
|
||||
detectHardware,
|
||||
isOpenSource,
|
||||
} from "../format";
|
||||
import type { User, Worker, StratumClient } from "../types";
|
||||
import type { Worker, StratumClient } from "../types";
|
||||
|
||||
const address = $derived(selection.user);
|
||||
|
||||
const user = $derived.by<User | null>(() => {
|
||||
if (!address) return null;
|
||||
return (snap.data?.users ?? []).find((u) => u.user === address) ?? null;
|
||||
// Workers belonging to this user. ckpool keys workers by full
|
||||
// workername (e.g. <btc>.label) and tracks user = the BTC string.
|
||||
const myWorkers = $derived.by<Worker[]>(() => {
|
||||
if (!address) return [];
|
||||
return (snap.data?.workers ?? []).filter((w) => w.user === address);
|
||||
});
|
||||
|
||||
const workerRows = $derived.by(() => {
|
||||
// Live stratum clients for those workers. ckpool's client.address
|
||||
// is the SOURCE IP, never the BTC, so we cannot filter on it. The
|
||||
// join key is workername — match the BTC bare or any "btc.label".
|
||||
const myClients = $derived.by<StratumClient[]>(() => {
|
||||
if (!address) return [];
|
||||
const workers = (snap.data?.workers ?? []).filter((w) => w.user === address);
|
||||
const clients = (snap.data?.clients ?? []).filter(
|
||||
(c: StratumClient) => c.address === address,
|
||||
const prefix = address + ".";
|
||||
return (snap.data?.clients ?? []).filter(
|
||||
(c) => c.workername === address || c.workername.startsWith(prefix),
|
||||
);
|
||||
});
|
||||
|
||||
// Per-worker rows joining client + worker. Worker's idle flag is
|
||||
// separate from a client being absent: a worker is "online" only
|
||||
// when a stratum_instance currently exists for it.
|
||||
const workerRows = $derived.by(() => {
|
||||
const clientByWorker = new Map<string, StratumClient>();
|
||||
for (const c of clients) {
|
||||
const wname = c.workername || `${c.address}.unnamed`;
|
||||
for (const c of myClients) {
|
||||
const wname = c.workername || `${address}.unnamed`;
|
||||
clientByWorker.set(wname, c);
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
const rows: Array<{
|
||||
type Row = {
|
||||
worker: string;
|
||||
hardware: string;
|
||||
openSource: boolean;
|
||||
tls: boolean;
|
||||
sourceIp: string;
|
||||
hashrate1m: number;
|
||||
hashrate1h: number;
|
||||
diff: number;
|
||||
bestRound: number;
|
||||
bestSession: number;
|
||||
bestEver: number;
|
||||
lastShare: number;
|
||||
online: boolean;
|
||||
idle: boolean;
|
||||
}> = [];
|
||||
for (const w of workers as Worker[]) {
|
||||
};
|
||||
const rows: Row[] = [];
|
||||
for (const w of myWorkers) {
|
||||
const c = clientByWorker.get(w.worker);
|
||||
seen.add(w.worker);
|
||||
rows.push({
|
||||
worker: w.worker,
|
||||
hardware: c ? detectHardware(c.useragent) : "offline",
|
||||
hashrate1m: (c?.dsps1 ?? w.dsps1) * 2 ** 32,
|
||||
hashrate1h: (c?.dsps60 ?? w.dsps60) * 2 ** 32,
|
||||
openSource: c ? isOpenSource(c.useragent) : false,
|
||||
tls: c?.server === 1,
|
||||
sourceIp: c?.address ?? "",
|
||||
hashrate1m: c ? c.dsps1 * 2 ** 32 : 0,
|
||||
hashrate1h: c ? c.dsps60 * 2 ** 32 : 0,
|
||||
diff: c?.diff ?? w.mindiff,
|
||||
// Session best = ckpool's stratum_instance.best_diff for the
|
||||
// currently-connected client only. Resets on miner disconnect
|
||||
// and pool restart. 0 for offline workers (no current session).
|
||||
bestRound: c?.bestdiff ?? 0,
|
||||
bestSession: c?.bestdiff ?? 0,
|
||||
bestEver: w.bestever,
|
||||
lastShare: c?.lastshare ?? w.lastshare,
|
||||
online: !!c,
|
||||
idle: !!(c?.idle ?? w.idle),
|
||||
});
|
||||
}
|
||||
for (const c of clients) {
|
||||
const wname = c.workername || `${c.address}.unnamed`;
|
||||
// A live client without a worker_instance entry yet (rare —
|
||||
// can happen briefly right after a new miner connects).
|
||||
for (const c of myClients) {
|
||||
const wname = c.workername || `${address}.unnamed`;
|
||||
if (seen.has(wname)) continue;
|
||||
rows.push({
|
||||
worker: wname,
|
||||
hardware: detectHardware(c.useragent),
|
||||
openSource: isOpenSource(c.useragent),
|
||||
tls: c.server === 1,
|
||||
sourceIp: c.address,
|
||||
hashrate1m: c.dsps1 * 2 ** 32,
|
||||
hashrate1h: c.dsps60 * 2 ** 32,
|
||||
diff: c.diff,
|
||||
bestRound: c.bestdiff,
|
||||
bestSession: c.bestdiff,
|
||||
bestEver: 0,
|
||||
lastShare: c.lastshare,
|
||||
online: true,
|
||||
@@ -79,36 +100,46 @@
|
||||
return rows;
|
||||
});
|
||||
|
||||
// Per-user totals: hashrate is summed across LIVE CLIENTS only,
|
||||
// not derived from user.dsps* (which decay slowly and would show
|
||||
// residual hashrate for a freshly-disconnected worker). Best ever
|
||||
// is the lifetime persistent counter from the user's workers.
|
||||
const totals = $derived.by(() => {
|
||||
if (user) {
|
||||
return {
|
||||
hs1m: user.dsps1 * 2 ** 32,
|
||||
hs5m: user.dsps5 * 2 ** 32,
|
||||
hs1h: user.dsps60 * 2 ** 32,
|
||||
hs24h: user.dsps1440 * 2 ** 32,
|
||||
bestDiff: user.bestdiff,
|
||||
bestEver: user.bestever,
|
||||
workers: user.workers,
|
||||
lastShare: user.lastshare,
|
||||
};
|
||||
}
|
||||
let hs1m = 0, hs1h = 0;
|
||||
let bestDiff = 0, bestEver = 0, lastShare = 0;
|
||||
for (const r of workerRows) {
|
||||
hs1m += r.hashrate1m;
|
||||
hs1h += r.hashrate1h;
|
||||
if (r.bestRound > bestDiff) bestDiff = r.bestRound;
|
||||
if (r.bestEver > bestEver) bestEver = r.bestEver;
|
||||
if (r.lastShare > lastShare) lastShare = r.lastShare;
|
||||
}
|
||||
const onlineCount = myClients.length;
|
||||
const hs1m = myClients.reduce((s, c) => s + c.dsps1 * 2 ** 32, 0);
|
||||
const hs5m = myClients.reduce((s, c) => s + c.dsps5 * 2 ** 32, 0);
|
||||
const hs1h = myClients.reduce((s, c) => s + c.dsps60 * 2 ** 32, 0);
|
||||
const hs24h = myClients.reduce((s, c) => s + c.dsps1440 * 2 ** 32, 0);
|
||||
const bestEver = myWorkers.reduce(
|
||||
(m, w) => Math.max(m, w.bestever || 0),
|
||||
0,
|
||||
);
|
||||
const bestSession = myClients.reduce(
|
||||
(m, c) => Math.max(m, c.bestdiff || 0),
|
||||
0,
|
||||
);
|
||||
const lastShare = Math.max(
|
||||
0,
|
||||
...myWorkers.map((w) => w.lastshare),
|
||||
...myClients.map((c) => c.lastshare),
|
||||
);
|
||||
return {
|
||||
hs1m, hs5m: 0, hs1h, hs24h: 0,
|
||||
bestDiff, bestEver,
|
||||
workers: workerRows.length,
|
||||
hs1m, hs5m, hs1h, hs24h,
|
||||
bestEver,
|
||||
bestSession,
|
||||
onlineWorkers: onlineCount,
|
||||
totalWorkers: myWorkers.length,
|
||||
lastShare,
|
||||
};
|
||||
});
|
||||
|
||||
const explorerBase = $derived.by(() => {
|
||||
const chain = snap.data?.chain?.chain ?? "main";
|
||||
if (chain === "test" || chain === "testnet4") return "https://mempool.space/testnet4";
|
||||
if (chain === "signet") return "https://mempool.space/signet";
|
||||
return "https://mempool.space";
|
||||
});
|
||||
|
||||
function onKey(ev: KeyboardEvent): void {
|
||||
if (ev.key === "Escape") clearSelection();
|
||||
}
|
||||
@@ -126,32 +157,46 @@
|
||||
<header class="head">
|
||||
<div class="stat-label">User</div>
|
||||
<h2 class="addr mono">{address}</h2>
|
||||
{#if address}
|
||||
<a
|
||||
class="explorer-link"
|
||||
href="{explorerBase}/address/{address}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>View on mempool.space →</a>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<section class="totals grid-4">
|
||||
<section class="totals">
|
||||
<div class="card">
|
||||
<div class="stat-label">Hashrate</div>
|
||||
<div class="stat-value">{formatHashrate(totals.hs1m)}</div>
|
||||
<div class="stat-sub">
|
||||
{#if totals.hs5m > 0}5m {formatHashrate(totals.hs5m)} · {/if}
|
||||
1h {formatHashrate(totals.hs1h)}
|
||||
{#if totals.hs24h > 0} · 24h {formatHashrate(totals.hs24h)}{/if}
|
||||
5m {formatHashrate(totals.hs5m)} ·
|
||||
1h {formatHashrate(totals.hs1h)} ·
|
||||
24h {formatHashrate(totals.hs24h)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="stat-label">Best share (ever)</div>
|
||||
<div class="stat-value">{formatDifficulty(totals.bestEver || totals.bestDiff)}</div>
|
||||
<div class="stat-sub">across all this user's workers</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="stat-label">Workers</div>
|
||||
<div class="stat-value">{totals.workers}</div>
|
||||
<div class="stat-sub">{workerRows.filter(r => r.online).length} online</div>
|
||||
<div class="stat-value">{totals.onlineWorkers} / {totals.totalWorkers}</div>
|
||||
<div class="stat-sub">online / total seen</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="stat-label">Last share</div>
|
||||
<div class="stat-value">{formatAgo(totals.lastShare)}</div>
|
||||
<div class="stat-sub">across all this user's workers</div>
|
||||
<div class="stat-label">Best (session)</div>
|
||||
<div class="stat-value">{formatDifficulty(totals.bestSession)}</div>
|
||||
<div class="stat-sub">resets on disconnect or solve</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="stat-label">Best (ever)</div>
|
||||
<div class="stat-value">{formatDifficulty(totals.bestEver)}</div>
|
||||
<div class="stat-sub">
|
||||
{#if totals.lastShare}
|
||||
last share {formatAgo(totals.lastShare)}
|
||||
{:else}
|
||||
no shares yet
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -166,6 +211,7 @@
|
||||
<tr>
|
||||
<th>Worker</th>
|
||||
<th>Hardware</th>
|
||||
<th>Status</th>
|
||||
<th class="num">Hashrate (1m)</th>
|
||||
<th class="num">Hashrate (1h)</th>
|
||||
<th class="num">Diff</th>
|
||||
@@ -177,12 +223,36 @@
|
||||
<tbody>
|
||||
{#each workerRows as r (r.worker)}
|
||||
<tr class:offline={!r.online} class:idle={r.idle}>
|
||||
<td class="mono">{r.worker}</td>
|
||||
<td>{r.hardware}</td>
|
||||
<td>
|
||||
<div class="wname mono">
|
||||
{r.worker}
|
||||
{#if r.tls}
|
||||
<span class="badge tls" title="Connected via TLS">TLS</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if r.sourceIp}
|
||||
<div class="src-ip mono">{r.sourceIp}</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{r.hardware}
|
||||
{#if r.openSource}
|
||||
<span class="oss" title="Open source hardware or firmware">★</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if !r.online}
|
||||
<span class="status offline">offline</span>
|
||||
{:else if r.idle}
|
||||
<span class="status idle">idle</span>
|
||||
{:else}
|
||||
<span class="status online">online</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="num">{formatHashrate(r.hashrate1m)}</td>
|
||||
<td class="num">{formatHashrate(r.hashrate1h)}</td>
|
||||
<td class="num">{formatDifficulty(r.diff)}</td>
|
||||
<td class="num">{formatDifficulty(r.bestRound)}</td>
|
||||
<td class="num">{formatDifficulty(r.bestSession)}</td>
|
||||
<td class="num">{formatDifficulty(r.bestEver)}</td>
|
||||
<td class="num">{formatAgo(r.lastShare)}</td>
|
||||
</tr>
|
||||
@@ -230,6 +300,19 @@
|
||||
word-break: break-all;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.explorer-link {
|
||||
color: var(--accent);
|
||||
font-size: 0.9em;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dashed var(--accent-dim);
|
||||
align-self: flex-start;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.explorer-link:hover {
|
||||
color: #ff9a5f;
|
||||
border-bottom-color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
.totals {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@@ -257,8 +340,60 @@
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.wname {
|
||||
font-size: 0.95em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.src-ip {
|
||||
color: var(--fg-dim);
|
||||
font-size: 0.75em;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
.badge.tls {
|
||||
display: inline-block;
|
||||
font-size: 0.65em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--good);
|
||||
border: 1px solid #2f5c48;
|
||||
background: #0f2419;
|
||||
padding: 0.05em 0.45em;
|
||||
border-radius: 999px;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
.oss {
|
||||
color: var(--accent);
|
||||
font-size: 0.95em;
|
||||
margin-left: 0.3em;
|
||||
cursor: help;
|
||||
}
|
||||
.status {
|
||||
font-size: 0.78em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.1em 0.55em;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.status.online {
|
||||
color: var(--good);
|
||||
border-color: #2f5c48;
|
||||
background: #0f2419;
|
||||
}
|
||||
.status.idle {
|
||||
color: var(--warn);
|
||||
border-color: #5c502f;
|
||||
background: #241f10;
|
||||
}
|
||||
.status.offline {
|
||||
color: var(--fg-dim);
|
||||
background: var(--bg-alt);
|
||||
}
|
||||
tr.offline {
|
||||
opacity: 0.5;
|
||||
opacity: 0.55;
|
||||
}
|
||||
tr.idle td {
|
||||
color: var(--fg-dim);
|
||||
|
||||
@@ -58,6 +58,9 @@ export type StratumClient = {
|
||||
dsps1440: number;
|
||||
lastshare: number;
|
||||
starttime: number;
|
||||
// Source IP, NOT the BTC payout address. ckpool sets this from
|
||||
// inet_ntop(client_addr) in connector.c — see stratum_add_instance.
|
||||
// The BTC address comes from `workername` (or the joined Worker.user).
|
||||
address: string;
|
||||
subscribed: boolean;
|
||||
authorised: boolean;
|
||||
@@ -65,6 +68,11 @@ export type StratumClient = {
|
||||
useragent: string;
|
||||
workername: string;
|
||||
userid: number;
|
||||
// Index into ckpool's serverurl[] array; identifies which stratum
|
||||
// bind the client connected on. We use this to tag TLS clients:
|
||||
// index 0 is the public plaintext bind, index 1 is the loopback-only
|
||||
// bind that stunnel forwards TLS traffic to.
|
||||
server: number;
|
||||
bestdiff: number;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user