From c2c376f8b958f74b5e5b122f88e0101521547800 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 6 May 2026 18:47:51 +0100 Subject: [PATCH] Add miner shameboard calculations --- .env.example | 10 +- apps/api/src/datum/parse.ts | 3 + apps/api/src/datum/profiles.ts | 4 + apps/api/src/datum/types.ts | 3 + apps/web/src/components/Shameboard.vue | 364 +++++++++++++++++++++++++ apps/web/src/stores/stats.ts | 3 + apps/web/src/strings.ts | 2 +- apps/web/src/types.ts | 3 + apps/web/src/views/DashboardView.vue | 3 + apps/web/src/views/GraphsView.vue | 3 + 10 files changed, 393 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/components/Shameboard.vue diff --git a/.env.example b/.env.example index e266664..98d397e 100644 --- a/.env.example +++ b/.env.example @@ -13,10 +13,12 @@ LOG_LEVEL=info # STATIC_DIR= # ---- Datum gateway (the Umbrel app we're polling) ---- -# Inside the Umbrel docker network, use Datum's service hostname directly. -# If you run the API outside that Docker network, override this with a hostname -# or IP address reachable from that process. -DATUM_URL=http://datum:21000 +# Portainer on Umbrel runs stacks inside Docker-in-Docker with host networking. +# Use Datum's real Umbrel Docker IP. Do not use datum/datum_datum_1 DNS and do +# not use host port 21000, which is Umbrel's auth proxy. +# Find it with: +# docker inspect -f '{{.NetworkSettings.Networks.umbrel_main_network.IPAddress}}' datum_datum_1 +DATUM_URL=http://10.21.0.11:21000 DATUM_ADMIN_USER=admin DATUM_ADMIN_PASSWORD= # How often to scrape /clients (ms). Datum updates per-worker hashrate every diff --git a/apps/api/src/datum/parse.ts b/apps/api/src/datum/parse.ts index f1547ed..48f9f5c 100644 --- a/apps/api/src/datum/parse.ts +++ b/apps/api/src/datum/parse.ts @@ -9,6 +9,7 @@ const UNKNOWN_PROFILE: MinerProfile = { ownerLabel: "unknown", locationLabel: "unknown", expectedHashrateThs: 0, + watts: 0, workerNameMatchers: [], }; @@ -119,6 +120,8 @@ export function parseClientsHtml(html: string): MinerStat[] { nickname: profile.nickname, model: profile.model, location: profile.locationLabel, + expectedHashrateThs: profile.expectedHashrateThs, + watts: profile.watts, hashrateThs, hashrateAgeS, lastShareAgeS, diff --git a/apps/api/src/datum/profiles.ts b/apps/api/src/datum/profiles.ts index a270c32..331d1df 100644 --- a/apps/api/src/datum/profiles.ts +++ b/apps/api/src/datum/profiles.ts @@ -12,6 +12,7 @@ export const MINER_PROFILES: readonly MinerProfile[] = [ ownerLabel: "shared", locationLabel: "Site A", expectedHashrateThs: 4.5, + watts: 80, workerNameMatchers: ["nerdqaxe", "qu4ck", "quack"], }, { @@ -21,6 +22,7 @@ export const MINER_PROFILES: readonly MinerProfile[] = [ ownerLabel: "shared", locationLabel: "Site A", expectedHashrateThs: 1.2, + watts: 18, workerNameMatchers: ["bitaxe", "p1xel", "pixel"], }, { @@ -30,6 +32,7 @@ export const MINER_PROFILES: readonly MinerProfile[] = [ ownerLabel: "shared", locationLabel: "Site B", expectedHashrateThs: 4.0, + watts: 140, workerNameMatchers: ["nano", "nano3", "n4n0"], }, { @@ -39,6 +42,7 @@ export const MINER_PROFILES: readonly MinerProfile[] = [ ownerLabel: "shared", locationLabel: "Site B", expectedHashrateThs: 37.0, + watts: 800, workerNameMatchers: ["mini", "mini3", "m1n1"], }, ] as const; diff --git a/apps/api/src/datum/types.ts b/apps/api/src/datum/types.ts index e97f482..0047222 100644 --- a/apps/api/src/datum/types.ts +++ b/apps/api/src/datum/types.ts @@ -5,6 +5,7 @@ export type MinerProfile = { ownerLabel: string; locationLabel: string; expectedHashrateThs: number; + watts: number; workerNameMatchers: readonly string[]; }; @@ -14,6 +15,8 @@ export type MinerStat = { nickname: string; model: string; location: string; + expectedHashrateThs: number; + watts: number; hashrateThs: number; hashrateAgeS: number | null; lastShareAgeS: number | null; diff --git a/apps/web/src/components/Shameboard.vue b/apps/web/src/components/Shameboard.vue new file mode 100644 index 0000000..5760eda --- /dev/null +++ b/apps/web/src/components/Shameboard.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/apps/web/src/stores/stats.ts b/apps/web/src/stores/stats.ts index a6955f2..2fe24eb 100644 --- a/apps/web/src/stores/stats.ts +++ b/apps/web/src/stores/stats.ts @@ -25,11 +25,13 @@ function asHistoryPoint(snap: DatumSnapshot): HistoryPoint { const miners: Record = {}; const minerWork: Record = {}; const minerShares: Record = {}; + const minerStatus: Record = {}; for (const miner of snap.miners) { const key = miner.nickname || miner.authUsername || miner.remoteHost; miners[key] = miner.hashrateThs; minerWork[key] = miner.diffAcceptedSum; minerShares[key] = miner.diffAcceptedCount; + minerStatus[key] = miner.status; } return { t: snap.fetchedAt, @@ -42,6 +44,7 @@ function asHistoryPoint(snap: DatumSnapshot): HistoryPoint { miners, minerWork, minerShares, + minerStatus, }; } diff --git a/apps/web/src/strings.ts b/apps/web/src/strings.ts index 6e3ec16..346c2f2 100644 --- a/apps/web/src/strings.ts +++ b/apps/web/src/strings.ts @@ -4,7 +4,7 @@ export const ROASTS: Record = { nerdqaxe: "wired together by a guy on Telegram. somehow still hashing.", bitaxe: "single chip, single mum, one in 10²² odds. heart of gold.", - "avalon-nano-3": "USB-stick miner. AliExpress's revenge on the network hashrate.", + "avalon-nano-3": "desk ornament with a fan. occasionally impersonates infrastructure.", "avalon-mini-3": "the only one of you that pays for itself. don't get a big head.", unknown: "we don't know what this is. we love it anyway.", }; diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index b4df148..7e7bbfd 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -4,6 +4,8 @@ export type MinerStat = { nickname: string; model: string; location: string; + expectedHashrateThs: number; + watts: number; hashrateThs: number; hashrateAgeS: number | null; lastShareAgeS: number | null; @@ -67,6 +69,7 @@ export type HistoryPoint = { miners: Record; minerWork: Record; minerShares: Record; + minerStatus: Record; }; export type LoginResponse = { diff --git a/apps/web/src/views/DashboardView.vue b/apps/web/src/views/DashboardView.vue index a53b3ce..43621b8 100644 --- a/apps/web/src/views/DashboardView.vue +++ b/apps/web/src/views/DashboardView.vue @@ -6,6 +6,7 @@ import PoolHero from "../components/PoolHero.vue"; import LotteryWidget from "../components/LotteryWidget.vue"; import ShareTicker from "../components/ShareTicker.vue"; import BlockCelebration from "../components/BlockCelebration.vue"; +import Shameboard from "../components/Shameboard.vue"; const stats = useStatsStore(); @@ -66,6 +67,8 @@ function minerKey(m: { nickname: string; authUsername: string; remoteHost: strin + +
+ +