Add per-worker stats page with cumulative work and luck

Expose ckpool's per-worker cumulative shares via patch 0004, add a
WorkerDetailPage with hashrate, status, best share with per-worker luck
(bestever/shares*100), and total work with pool share percentage.
Worker names are now clickable in MinersTable and UserDetailPage.
Also centers numeric columns below their headers.
This commit is contained in:
satoshi
2026-05-10 18:02:08 +03:00
parent 35ee3208b6
commit e622f1a81a
9 changed files with 458 additions and 27 deletions
+33 -22
View File
@@ -1,30 +1,38 @@
// Simple hash-based routing for user pages. The URL hash is the
// Simple hash-based routing for detail pages. The URL hash is the
// source of truth:
// #/ -> dashboard
// #/user/<address> -> per-user page
// #/ -> dashboard
// #/user/<address> -> per-user page
// #/worker/<workername> -> per-worker page
//
// selectUser / clearSelection just mutate the hash; a hashchange
// listener reads it back into reactive state so any component in the
// tree re-renders when the route changes.
// selectUser / selectWorker / clearSelection just mutate the hash;
// a hashchange listener reads it back into reactive state so any
// component in the tree re-renders when the route changes.
const USER_PREFIX = "#/user/";
const WORKER_PREFIX = "#/worker/";
export const selection = $state<{ user: string | null }>({
user: readHash(),
});
type Selection = { user: string | null; worker: string | null };
function readHash(): string | null {
if (typeof window === "undefined") return null;
export const selection = $state<Selection>(readHash());
function readHash(): Selection {
if (typeof window === "undefined") return { user: null, worker: null };
const h = window.location.hash;
if (h.startsWith(USER_PREFIX)) {
const addr = decodeURIComponent(h.slice(USER_PREFIX.length));
return addr || null;
if (h.startsWith(WORKER_PREFIX)) {
const w = decodeURIComponent(h.slice(WORKER_PREFIX.length));
return { user: null, worker: w || null };
}
return null;
if (h.startsWith(USER_PREFIX)) {
const u = decodeURIComponent(h.slice(USER_PREFIX.length));
return { user: u || null, worker: null };
}
return { user: null, worker: null };
}
function syncFromHash(): void {
selection.user = readHash();
const parsed = readHash();
selection.user = parsed.user;
selection.worker = parsed.worker;
}
if (typeof window !== "undefined") {
@@ -32,16 +40,19 @@ if (typeof window !== "undefined") {
}
export function selectUser(address: string): void {
// Set the hash; the hashchange listener updates selection.user so
// routing-style state stays in one place.
window.location.hash = USER_PREFIX + encodeURIComponent(address);
}
export function selectWorker(workername: string): void {
window.location.hash = WORKER_PREFIX + encodeURIComponent(workername);
}
export function clearSelection(): void {
// Prefer history.back when we arrived here via selectUser so the
// browser back arrow does the obvious thing; otherwise just drop
// the hash so a fresh page load starts on the dashboard.
if (window.history.length > 1 && window.location.hash.startsWith(USER_PREFIX)) {
if (
window.history.length > 1 &&
(window.location.hash.startsWith(USER_PREFIX) ||
window.location.hash.startsWith(WORKER_PREFIX))
) {
window.history.back();
} else {
window.location.hash = "";