Tighten OSS detection, refresh TLS badge, plumb custom mempool URL
isOpenSource was matching things like Braiins OS, cgminer, bfgminer,
and a generic "esp32" — software that's open but runs on closed
hardware (Antminer, unknown rigs). public-pool's UI reserves the
star for open-source HARDWARE, with open firmware on top, and that's
the convention to match. New regex covers the family verbatim from
public-pool-ui's user-agent-link switch table:
bitaxe, bitaxeHex, NerdMiner, NerdNOS, NerdAxe, NerdAxeGamma,
NerdOCTAXE, NerdEKO, NerdQAxe+, NerdQAxe++, PiAxe, QAxe, QAxe+,
0xAxe, LeafMiner
Also extend detectHardware so all those user-agents get a recognised
label instead of "esp32" / raw token.
Replace the squished "TLS" pill with an inline-flex badge: 0.78em
text, 5px corners, padlock SVG, generous left/right padding so it
reads at a glance instead of looking like a typo.
Custom block explorer: new optional union under StartOS Advanced
config ("Block Explorer" — defaults to "mempool.space"). Picking
"Custom URL" surfaces the value as MEMPOOL_BASE_URL on the
container env. config.Config picks it up, the aggregator copies
it into every Snapshot (mempool_base_url field), and the UI's
shared explorerBaseFor() helper trusts the custom URL verbatim
when present (no /testnet4 / /signet path appended — a self-hosted
instance is presumably single-network already). Falls back to the
public mempool.space mirrors per chain when unset, which is the
default behaviour.
This commit is contained in:
@@ -60,6 +60,7 @@ func main() {
|
|||||||
|
|
||||||
agg := state.New(ck, rpc, cfg.PollInterval, log)
|
agg := state.New(ck, rpc, cfg.PollInterval, log)
|
||||||
agg.Store = blockStore
|
agg.Store = blockStore
|
||||||
|
agg.MempoolBaseURL = cfg.MempoolBaseURL
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ type Config struct {
|
|||||||
|
|
||||||
// Poll interval for refreshing ckpool stats
|
// Poll interval for refreshing ckpool stats
|
||||||
PollInterval time.Duration
|
PollInterval time.Duration
|
||||||
|
|
||||||
|
// Optional mempool.space-compatible explorer base URL. Empty means
|
||||||
|
// the UI uses the public mempool.space; non-empty means the user
|
||||||
|
// has pointed Kamado at their own instance via the StartOS config.
|
||||||
|
MempoolBaseURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromEnv() (*Config, error) {
|
func FromEnv() (*Config, error) {
|
||||||
@@ -48,6 +53,7 @@ func FromEnv() (*Config, error) {
|
|||||||
CKPoolLogFile: getenv("CKPOOL_LOGFILE", "/var/log/ckpool/ckpool.log"),
|
CKPoolLogFile: getenv("CKPOOL_LOGFILE", "/var/log/ckpool/ckpool.log"),
|
||||||
DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"),
|
DBPath: getenv("DB_PATH", "/var/lib/kamado/kamado.db"),
|
||||||
PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second),
|
PollInterval: getenvDuration("POLL_INTERVAL", 5*time.Second),
|
||||||
|
MempoolBaseURL: os.Getenv("MEMPOOL_BASE_URL"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.BitcoinRPCURL == "" {
|
if cfg.BitcoinRPCURL == "" {
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ type Snapshot struct {
|
|||||||
// 24-hour hashrate history, sampled once per minute (max 1440 points).
|
// 24-hour hashrate history, sampled once per minute (max 1440 points).
|
||||||
HashrateHistory []HashratePoint `json:"hashrate_history,omitempty"`
|
HashrateHistory []HashratePoint `json:"hashrate_history,omitempty"`
|
||||||
|
|
||||||
|
// Optional override for explorer links in the UI. Empty means use
|
||||||
|
// mempool.space defaults. Set via MEMPOOL_BASE_URL env, surfaced by
|
||||||
|
// the StartOS config "Block Explorer" -> "Custom URL".
|
||||||
|
MempoolBaseURL string `json:"mempool_base_url,omitempty"`
|
||||||
|
|
||||||
// Health
|
// Health
|
||||||
CKPoolOK bool `json:"ckpool_ok"`
|
CKPoolOK bool `json:"ckpool_ok"`
|
||||||
BitcoinOK bool `json:"bitcoin_ok"`
|
BitcoinOK bool `json:"bitcoin_ok"`
|
||||||
@@ -93,6 +98,10 @@ type Aggregator struct {
|
|||||||
Interval time.Duration
|
Interval time.Duration
|
||||||
Log *slog.Logger
|
Log *slog.Logger
|
||||||
|
|
||||||
|
// Static config that the UI consumes via the snapshot. Empty
|
||||||
|
// MempoolBaseURL leaves the UI on its mempool.space defaults.
|
||||||
|
MempoolBaseURL string
|
||||||
|
|
||||||
// OnRefresh, if set, is called (non-blocking) after each snapshot
|
// OnRefresh, if set, is called (non-blocking) after each snapshot
|
||||||
// refresh. Used by the WebSocket hub to push updates to clients.
|
// refresh. Used by the WebSocket hub to push updates to clients.
|
||||||
OnRefresh func(Snapshot)
|
OnRefresh func(Snapshot)
|
||||||
@@ -187,7 +196,7 @@ func (a *Aggregator) refresh(ctx context.Context) {
|
|||||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
next := Snapshot{GeneratedAt: time.Now()}
|
next := Snapshot{GeneratedAt: time.Now(), MempoolBaseURL: a.MempoolBaseURL}
|
||||||
|
|
||||||
// --- ckpool: poolstats, users, workers, clients, uptime ---
|
// --- ckpool: poolstats, users, workers, clients, uptime ---
|
||||||
if ps, err := a.CK.PoolStats(ctx); err == nil {
|
if ps, err := a.CK.PoolStats(ctx); err == nil {
|
||||||
|
|||||||
+38
-8
@@ -63,14 +63,16 @@ export function formatAgo(unixSeconds: number): string {
|
|||||||
return `${Math.floor(ageSec / 86400)}d ago`;
|
return `${Math.floor(ageSec / 86400)}d ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open-source ASIC hardware (Bitaxe family, NerdMiner, etc.) and
|
// Star = open-source HARDWARE *and* SOFTWARE, matching public-pool's
|
||||||
// open-source firmware projects (Braiins OS, cgminer/bfgminer/
|
// classification at https://github.com/benjamin-wilson/public-pool-ui
|
||||||
// ckminer). Used to render a star next to the hardware label, like
|
// (user-agent-link.component.html). Closed hardware running open
|
||||||
// public-pool does, so visitors can spot DIY / OSS rigs at a glance.
|
// firmware (Braiins OS on Antminer) doesn't qualify, and neither do
|
||||||
|
// generic CPU/cgminer agents that don't tell us what hardware
|
||||||
|
// they're running on.
|
||||||
export function isOpenSource(ua: string): boolean {
|
export function isOpenSource(ua: string): boolean {
|
||||||
if (!ua) return false;
|
if (!ua) return false;
|
||||||
const s = ua.toLowerCase();
|
const s = ua.toLowerCase();
|
||||||
return /bitaxe|nerdaxe|nerdqaxe|nerdoctaxe|nerdminer|qaxe|mccm|lucky|braiins|cgminer|bfgminer|ckminer|esp32/.test(s);
|
return /bitaxe|nerdminer|nerdnos|nerdaxe|nerdoctaxe|nerdeko|nerdqaxe|piaxe|qaxe|0xaxe|leafminer/.test(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the BTC payout address from a stratum username. Stratum
|
// Extract the BTC payout address from a stratum username. Stratum
|
||||||
@@ -82,6 +84,23 @@ export function btcAddressOf(workername: string): string {
|
|||||||
return dot < 0 ? workername : workername.slice(0, dot);
|
return dot < 0 ? workername : workername.slice(0, dot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the explorer base URL for the active network. If the user
|
||||||
|
// configured a custom mempool instance via StartOS, we trust that
|
||||||
|
// verbatim and don't append a /testnet4 / /signet path — the user's
|
||||||
|
// instance presumably runs on a single network already. Otherwise
|
||||||
|
// fall back to mempool.space with the appropriate network prefix.
|
||||||
|
export function explorerBaseFor(
|
||||||
|
chain: string | undefined,
|
||||||
|
customBaseUrl: string | undefined,
|
||||||
|
): string {
|
||||||
|
if (customBaseUrl && customBaseUrl.length > 0) {
|
||||||
|
return customBaseUrl.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
if (chain === "test" || chain === "testnet4") return "https://mempool.space/testnet4";
|
||||||
|
if (chain === "signet") return "https://mempool.space/signet";
|
||||||
|
return "https://mempool.space";
|
||||||
|
}
|
||||||
|
|
||||||
// Detect common Bitcoin mining hardware from the stratum useragent
|
// Detect common Bitcoin mining hardware from the stratum useragent
|
||||||
// string. This is a best-effort heuristic; unknown agents fall back
|
// string. This is a best-effort heuristic; unknown agents fall back
|
||||||
// to a stripped version of the raw string. Order matters — check
|
// to a stripped version of the raw string. Order matters — check
|
||||||
@@ -90,14 +109,23 @@ export function detectHardware(ua: string): string {
|
|||||||
if (!ua) return "unknown";
|
if (!ua) return "unknown";
|
||||||
const s = ua.toLowerCase();
|
const s = ua.toLowerCase();
|
||||||
const rules: Array<[RegExp, string]> = [
|
const rules: Array<[RegExp, string]> = [
|
||||||
|
// Bitaxe and Nerd-family open-source ASICs. Order matters — check
|
||||||
|
// narrower variants before the broader Nerd* / Bitaxe roots.
|
||||||
|
[/bitaxehex/, "Bitaxe Hex"],
|
||||||
[/bitaxe/, "Bitaxe"],
|
[/bitaxe/, "Bitaxe"],
|
||||||
|
[/nerdqaxe\+\+/, "NerdQAxe++"],
|
||||||
[/nerdqaxe/, "NerdQAxe+"],
|
[/nerdqaxe/, "NerdQAxe+"],
|
||||||
|
[/nerdaxegamma/, "NerdAxeGamma"],
|
||||||
[/nerdaxe/, "NerdAxe"],
|
[/nerdaxe/, "NerdAxe"],
|
||||||
|
[/nerdoctaxe/, "NerdOCTAXE"],
|
||||||
|
[/nerdeko/, "NerdEKO"],
|
||||||
|
[/nerdnos/, "NerdNOS"],
|
||||||
[/nerdminer/, "NerdMiner"],
|
[/nerdminer/, "NerdMiner"],
|
||||||
[/nerdoctaxe/, "NerdOctaxe"],
|
[/0xaxe/, "0xAxe"],
|
||||||
[/mccm/, "MCCM"],
|
[/qaxe\+/, "QAxe+"],
|
||||||
[/lucky(?:miner)?/, "Lucky Miner"],
|
|
||||||
[/qaxe/, "QAxe"],
|
[/qaxe/, "QAxe"],
|
||||||
|
[/piaxe/, "PiAxe"],
|
||||||
|
[/leafminer/, "LeafMiner"],
|
||||||
[/antminer/, "Antminer"],
|
[/antminer/, "Antminer"],
|
||||||
[/whatsminer/, "Whatsminer"],
|
[/whatsminer/, "Whatsminer"],
|
||||||
[/avalon/, "Avalon"],
|
[/avalon/, "Avalon"],
|
||||||
@@ -105,7 +133,9 @@ export function detectHardware(ua: string): string {
|
|||||||
[/bfgminer/, "bfgminer"],
|
[/bfgminer/, "bfgminer"],
|
||||||
[/bmminer/, "BMMiner"],
|
[/bmminer/, "BMMiner"],
|
||||||
[/ckminer/, "ckminer"],
|
[/ckminer/, "ckminer"],
|
||||||
|
[/cpuminer/, "cpuminer"],
|
||||||
[/braiins/, "Braiins OS"],
|
[/braiins/, "Braiins OS"],
|
||||||
|
[/termux/, "termux-miner"],
|
||||||
[/micro[- ]?bt/, "MicroBT"],
|
[/micro[- ]?bt/, "MicroBT"],
|
||||||
[/esp32/, "ESP32"],
|
[/esp32/, "ESP32"],
|
||||||
[/s9/, "Antminer S9"],
|
[/s9/, "Antminer S9"],
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { snap } from "../stores/snapshot.svelte";
|
import { snap } from "../stores/snapshot.svelte";
|
||||||
import { formatAgo, formatDifficulty } from "../format";
|
import { formatAgo, formatDifficulty, explorerBaseFor } from "../format";
|
||||||
|
|
||||||
const blocks = $derived.by(() => {
|
const blocks = $derived.by(() => {
|
||||||
const b = snap.data?.recent_blocks ?? [];
|
const b = snap.data?.recent_blocks ?? [];
|
||||||
return [...b].reverse();
|
return [...b].reverse();
|
||||||
});
|
});
|
||||||
|
|
||||||
// mempool.space mirror picked per network. If chain isn't known, the
|
const explorerBase = $derived(
|
||||||
// mainnet link still works as a best effort.
|
explorerBaseFor(snap.data?.chain?.chain, snap.data?.mempool_base_url),
|
||||||
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";
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
|
|||||||
@@ -121,7 +121,12 @@
|
|||||||
<div class="wname mono">
|
<div class="wname mono">
|
||||||
{r.workerName}
|
{r.workerName}
|
||||||
{#if r.tls}
|
{#if r.tls}
|
||||||
<span class="badge tls" title="Connected via TLS">TLS</span>
|
<span class="badge tls" title="Connected via TLS">
|
||||||
|
<svg viewBox="0 0 14 14" aria-hidden="true">
|
||||||
|
<path d="M4 6V4.5a3 3 0 0 1 6 0V6h.5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1H4Zm1.4 0h3.2V4.5a1.6 1.6 0 0 0-3.2 0V6Z"/>
|
||||||
|
</svg>
|
||||||
|
<span>TLS</span>
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -201,17 +206,25 @@
|
|||||||
margin-top: 0.2em;
|
margin-top: 0.2em;
|
||||||
}
|
}
|
||||||
.badge.tls {
|
.badge.tls {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
font-size: 0.62em;
|
align-items: center;
|
||||||
|
gap: 0.32em;
|
||||||
|
font-size: 0.78em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.04em;
|
||||||
color: var(--good);
|
color: var(--good);
|
||||||
border: 1px solid #2f5c48;
|
border: 1px solid #3a7558;
|
||||||
background: #0f2419;
|
background: #112e22;
|
||||||
padding: 0.05em 0.45em;
|
padding: 0.18em 0.55em 0.18em 0.4em;
|
||||||
border-radius: 999px;
|
border-radius: 5px;
|
||||||
text-transform: none;
|
text-transform: uppercase;
|
||||||
vertical-align: 1px;
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.badge.tls svg {
|
||||||
|
width: 0.95em;
|
||||||
|
height: 0.95em;
|
||||||
|
fill: currentColor;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.oss {
|
.oss {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
formatAgo,
|
formatAgo,
|
||||||
detectHardware,
|
detectHardware,
|
||||||
isOpenSource,
|
isOpenSource,
|
||||||
|
explorerBaseFor,
|
||||||
} from "../format";
|
} from "../format";
|
||||||
import type { Worker, StratumClient } from "../types";
|
import type { Worker, StratumClient } from "../types";
|
||||||
|
|
||||||
@@ -133,12 +134,9 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const explorerBase = $derived.by(() => {
|
const explorerBase = $derived(
|
||||||
const chain = snap.data?.chain?.chain ?? "main";
|
explorerBaseFor(snap.data?.chain?.chain, snap.data?.mempool_base_url),
|
||||||
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 {
|
function onKey(ev: KeyboardEvent): void {
|
||||||
if (ev.key === "Escape") clearSelection();
|
if (ev.key === "Escape") clearSelection();
|
||||||
@@ -227,7 +225,12 @@
|
|||||||
<div class="wname mono">
|
<div class="wname mono">
|
||||||
{r.worker}
|
{r.worker}
|
||||||
{#if r.tls}
|
{#if r.tls}
|
||||||
<span class="badge tls" title="Connected via TLS">TLS</span>
|
<span class="badge tls" title="Connected via TLS">
|
||||||
|
<svg viewBox="0 0 14 14" aria-hidden="true">
|
||||||
|
<path d="M4 6V4.5a3 3 0 0 1 6 0V6h.5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1H4Zm1.4 0h3.2V4.5a1.6 1.6 0 0 0-3.2 0V6Z"/>
|
||||||
|
</svg>
|
||||||
|
<span>TLS</span>
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if r.sourceIp}
|
{#if r.sourceIp}
|
||||||
@@ -353,16 +356,25 @@
|
|||||||
margin-top: 0.2em;
|
margin-top: 0.2em;
|
||||||
}
|
}
|
||||||
.badge.tls {
|
.badge.tls {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
font-size: 0.65em;
|
align-items: center;
|
||||||
|
gap: 0.32em;
|
||||||
|
font-size: 0.78em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.04em;
|
||||||
color: var(--good);
|
color: var(--good);
|
||||||
border: 1px solid #2f5c48;
|
border: 1px solid #3a7558;
|
||||||
background: #0f2419;
|
background: #112e22;
|
||||||
padding: 0.05em 0.45em;
|
padding: 0.18em 0.55em 0.18em 0.4em;
|
||||||
border-radius: 999px;
|
border-radius: 5px;
|
||||||
vertical-align: 1px;
|
text-transform: uppercase;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.badge.tls svg {
|
||||||
|
width: 0.95em;
|
||||||
|
height: 0.95em;
|
||||||
|
fill: currentColor;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.oss {
|
.oss {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ export type Snapshot = {
|
|||||||
network_hashrate_hs: number;
|
network_hashrate_hs: number;
|
||||||
recent_blocks?: BlockRecord[];
|
recent_blocks?: BlockRecord[];
|
||||||
hashrate_history?: HashratePoint[];
|
hashrate_history?: HashratePoint[];
|
||||||
|
// Optional override for explorer links. Empty/undefined means the
|
||||||
|
// UI falls back to its mempool.space defaults.
|
||||||
|
mempool_base_url?: string;
|
||||||
ckpool_ok: boolean;
|
ckpool_ok: boolean;
|
||||||
bitcoin_ok: boolean;
|
bitcoin_ok: boolean;
|
||||||
last_error?: string;
|
last_error?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user