This commit is contained in:
2026-01-13 09:44:35 -05:00
parent 81fc4c8313
commit b3a906fd0c

143
installer
View File

@ -4,11 +4,11 @@
# An Open Source Linux Appliance from Robbie Ferguson # An Open Source Linux Appliance from Robbie Ferguson
# (c) 2025 Robbie Ferguson # (c) 2025 Robbie Ferguson
# Version 1.0.2 # Version 1.0.3
set -e set -e
BTC_VER="30.0" BTC_VER="29.2"
APP_ROOT="/opt/btc-solo" APP_ROOT="/opt/btc-solo"
WWW_ROOT="/var/www/btc-solo" WWW_ROOT="/var/www/btc-solo"
@ -109,7 +109,7 @@ curl -fsSL "$BASE_URL/$TARBALL" -o "$TMPD/$TARBALL"
curl -fsSL "$BASE_URL/SHA256SUMS" -o "$TMPD/SHA256SUMS" curl -fsSL "$BASE_URL/SHA256SUMS" -o "$TMPD/SHA256SUMS"
if command -v gpg >/dev/null 2>&1; then if command -v gpg >/dev/null 2>&1; then
curl -fsSL "$BASE_URL/SHA256SUMS.asc" -o "$TMPD/SHA256SUMS.asc" || true curl -fsSL "$BASE_URL/SHA256SUMS.asc" -o "$TMPD/SHA256SUMS.asc" || true
# If youve already imported the Bitcoin Core release keys, this will verify. # If you've already imported the Bitcoin Core release keys, this will verify.
# If not, we still proceed after SHA256 check below. # If not, we still proceed after SHA256 check below.
gpg --verify "$TMPD/SHA256SUMS.asc" "$TMPD/SHA256SUMS" || echo "[WARN] Could not verify SHA256SUMS signature (no keys?). Continuing with SHA256 check." gpg --verify "$TMPD/SHA256SUMS.asc" "$TMPD/SHA256SUMS" || echo "[WARN] Could not verify SHA256SUMS signature (no keys?). Continuing with SHA256 check."
fi fi
@ -1166,7 +1166,7 @@ function drawLineGraph(containerId, points, opts = {}) {
<span class="graph-legend-label">Range</span> <span class="graph-legend-label">Range</span>
<span class="graph-legend-value">${ <span class="graph-legend-value">${
isFinite(minVal) && isFinite(maxVal) isFinite(minVal) && isFinite(maxVal)
? minVal.toFixed(2) + '' + maxVal.toFixed(2) + ' ' + unit ? minVal.toFixed(2) + '-' + maxVal.toFixed(2) + ' ' + unit
: 'n/a' : 'n/a'
}</span> }</span>
</div> </div>
@ -1425,7 +1425,7 @@ async function loadStatus() {
const rawIbd = data.initialblockdownload; const rawIbd = data.initialblockdownload;
const hasRealIbd = (typeof rawIbd === 'boolean'); const hasRealIbd = (typeof rawIbd === 'boolean');
const ibd = hasRealIbd ? rawIbd : true; const ibd = hasRealIbd ? rawIbd : true;
const blocks = data.blocks ?? ''; const blocks = data.blocks ?? '-';
const pruned = data.pruned ? '(pruned)' : ''; const pruned = data.pruned ? '(pruned)' : '';
let progress = 0; let progress = 0;
if (typeof data.verificationprogress === 'number') { if (typeof data.verificationprogress === 'number') {
@ -1591,7 +1591,7 @@ function parseHashrate(str) {
} }
function formatHashrateHps(value) { function formatHashrateHps(value) {
if (value == null || !isFinite(value) || value <= 0) return ''; if (value == null || !isFinite(value) || value <= 0) return '-';
let v = value; let v = value;
let unit = 'H/s'; let unit = 'H/s';
@ -1627,9 +1627,9 @@ const HIDE_SEC = 3 * 24 * 3600; // 3 days -> "inactive" list
const PURGE_SEC = 30 * 24 * 3600; // 30 days -> purge from UI const PURGE_SEC = 30 * 24 * 3600; // 30 days -> purge from UI
function formatShare(value) { function formatShare(value) {
if (value == null) return ''; if (value == null) return '-';
const num = typeof value === 'number' ? value : parseFloat(value); const num = typeof value === 'number' ? value : parseFloat(value);
if (!isFinite(num)) return ''; if (!isFinite(num)) return '-';
const units = ['', 'k', 'M', 'G', 'T', 'P']; const units = ['', 'k', 'M', 'G', 'T', 'P'];
let v = num; let v = num;
let i = 0; let i = 0;
@ -1675,13 +1675,13 @@ function buildWorkerDetailHtml(worker, animate, graphId) {
detail.push('<div>Last share: ' + lastRel + ' (' + lastAbs + ')</div>'); detail.push('<div>Last share: ' + lastRel + ' (' + lastAbs + ')</div>');
detail.push('<div style="margin-top:.25rem;">Hashrate (averages):</div>'); detail.push('<div style="margin-top:.25rem;">Hashrate (averages):</div>');
detail.push('<ul style="margin:.2rem 0 .3rem .9rem;font-size:.84rem;">' + detail.push('<ul style="margin:.2rem 0 .3rem .9rem;font-size:.84rem;">' +
'<li>1 minute: ' + (worker.hashrate1m || '') + '</li>' + '<li>1 minute: ' + (worker.hashrate1m || '-') + '</li>' +
'<li>5 minutes: ' + (worker.hashrate5m || '') + '</li>' + '<li>5 minutes: ' + (worker.hashrate5m || '-') + '</li>' +
'<li>1 hour: ' + (worker.hashrate1hr || '') + '</li>' + '<li>1 hour: ' + (worker.hashrate1hr || '-') + '</li>' +
'<li>1 day: ' + (worker.hashrate1d || '') + '</li>' + '<li>1 day: ' + (worker.hashrate1d || '-') + '</li>' +
'<li>7 days: ' + (worker.hashrate7d || '') + '</li>' + '<li>7 days: ' + (worker.hashrate7d || '-') + '</li>' +
'</ul>'); '</ul>');
detail.push('<div>Shares: ' + (worker.shares ?? '') + detail.push('<div>Shares: ' + (worker.shares ?? '-') +
' • Best share: ' + formatShare(worker.bestshare) + ' • Best share: ' + formatShare(worker.bestshare) +
' • Best ever: ' + formatShare(worker.bestever) + '</div>'); ' • Best ever: ' + formatShare(worker.bestever) + '</div>');
@ -1816,17 +1816,17 @@ async function loadPool() {
const acceptedText = shares.accepted != null const acceptedText = shares.accepted != null
? acceptedNum.toLocaleString() ? acceptedNum.toLocaleString()
: ''; : '-';
const rejectedText = shares.rejected != null const rejectedText = shares.rejected != null
? rejectedNum.toLocaleString() ? rejectedNum.toLocaleString()
: ''; : '-';
let rejectPct = null; let rejectPct = null;
if (totalShares > 0 && rejectedNum >= 0) { if (totalShares > 0 && rejectedNum >= 0) {
rejectPct = ((rejectedNum / totalShares) * 100).toFixed(2); rejectPct = ((rejectedNum / totalShares) * 100).toFixed(2);
} }
const bestHashText = bestPoolH1m ? formatHashrateHps(bestPoolH1m) : ''; const bestHashText = bestPoolH1m ? formatHashrateHps(bestPoolH1m) : '-';
summaryHtml += '<div class="pool-metrics-label" style="margin-top:1rem;">SHARES</div>'; summaryHtml += '<div class="pool-metrics-label" style="margin-top:1rem;">SHARES</div>';
summaryHtml += '<div class="pool-shares-line">'; summaryHtml += '<div class="pool-shares-line">';
@ -1932,12 +1932,12 @@ async function loadPool() {
tableHtml += '<tr class="' + rowClass + openClass + '" data-worker="' + tableHtml += '<tr class="' + rowClass + openClass + '" data-worker="' +
encodeURIComponent(w.workername) + '">' + encodeURIComponent(w.workername) + '">' +
'<td>' + displayName + '</td>' + '<td>' + displayName + '</td>' +
'<td>' + (w.hashrate1m || '') + '</td>' + '<td>' + (w.hashrate1m || '-') + '</td>' +
'<td>' + (w.hashrate5m || '') + '</td>' + '<td>' + (w.hashrate5m || '-') + '</td>' +
'<td>' + (w.hashrate1hr || '') + '</td>' + '<td>' + (w.hashrate1hr || '-') + '</td>' +
'<td>' + (w.hashrate1d || '') + '</td>' + '<td>' + (w.hashrate1d || '-') + '</td>' +
'<td>' + (w.hashrate7d || '') + '</td>' + '<td>' + (w.hashrate7d || '-') + '</td>' +
'<td>' + (w.shares ?? '') + '</td>' + '<td>' + (w.shares ?? '-') + '</td>' +
'<td>' + formatShare(w.bestshare) + '</td>' + '<td>' + formatShare(w.bestshare) + '</td>' +
'<td>' + lastCol + '</td>' + '<td>' + lastCol + '</td>' +
'</tr>'; '</tr>';
@ -2349,7 +2349,7 @@ if (isset($_GET['addr'])) {
$addr = trim($_GET['addr']); $addr = trim($_GET['addr']);
} }
// light sanity check (not strict CKPool does the real validation) // light sanity check (not strict - CKPool does the real validation)
if ($addr !== '' && !preg_match('/^[13bc][a-km-zA-HJ-NP-Z1-9]{25,}$/', $addr)) { if ($addr !== '' && !preg_match('/^[13bc][a-km-zA-HJ-NP-Z1-9]{25,}$/', $addr)) {
$addr = ''; $addr = '';
} }
@ -2418,7 +2418,7 @@ if ($addr !== '') {
} }
$resp['worker_count'] = count($resp['workers']); $resp['worker_count'] = count($resp['workers']);
} else { } else {
// Not a fatal error just "no data yet" for that address // Not a fatal error - just "no data yet" for that address
if ($resp['error'] === null) { if ($resp['error'] === null) {
$resp['error'] = 'No worker stats yet for this address'; $resp['error'] = 'No worker stats yet for this address';
} }
@ -2483,7 +2483,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<form method="post"> <form method="post">
<h2>Create New Admin User</h2> <h2>Create New Admin User</h2>
<p><small> <p><small>
This sets the Bitcoin nodes administrative (RPC) credentials used to control the server itself.<br /><br /> This sets the Bitcoin node's administrative (RPC) credentials used to control the server itself.<br /><br />
Keep these private — they grant full access to your node.<br /><br /> Keep these private — they grant full access to your node.<br /><br />
These credentials are <b>not</b> used by miners to connect; miners only need your Bitcoin address and can use any password. These credentials are <b>not</b> used by miners to connect; miners only need your Bitcoin address and can use any password.
</small></p> </small></p>
@ -2742,10 +2742,10 @@ draw_dashboard() {
tput cup 13 0; echo -e "Memory: ${mem}\033[K" tput cup 13 0; echo -e "Memory: ${mem}\033[K"
tput cup 14 0; echo -e "Disk (${BTC_DIR}): ${disk}\033[K" tput cup 14 0; echo -e "Disk (${BTC_DIR}): ${disk}\033[K"
tput cup 16 0; echo -e "Workers:\033[K" tput cup 16 0; echo -e "Mining:\033[K"
tput cup 17 2; echo -e "${workers}\033[K" tput cup 17 2; echo -e "${workers}\033[K"
tput cup 19 0; echo -e "(Appliance console view — use the web UI for full details.)\033[K" # tput cup 19 0; echo -e "(Appliance console view — use the web UI for full details.)\033[K"
} }
draw_header draw_header
@ -2768,7 +2768,84 @@ while true; do
PROGRESS_LINE="" PROGRESS_LINE=""
BAR_LINE="[--------------] 0.0%" BAR_LINE="[--------------] 0.0%"
SERVICES_LINE="Bitcoin: ${BTC_STATE} CKPool: ${CKP_STATE}" SERVICES_LINE="Bitcoin: ${BTC_STATE} CKPool: ${CKP_STATE}"
WORKERS_LINE="No workers detected (point your ASICs at this node)"
# --- CKPool worker summary (console-safe, no per-worker listing) ---
POOL_STATUS="/opt/btc-solo/logs/pool/pool.status"
# Humanize hashrate (expects H/s as a number; falls back to raw if unknown)
human_hashrate() {
local v="$1"
if [[ -z "$v" || "$v" == "null" ]]; then echo "-"; return; fi
if ! [[ "$v" =~ ^[0-9]+([.][0-9]+)?$ ]]; then echo "$v"; return; fi
awk -v x="$v" 'BEGIN{
u[0]="H/s"; u[1]="kH/s"; u[2]="MH/s"; u[3]="GH/s"; u[4]="TH/s"; u[5]="PH/s"; u[6]="EH/s";
i=0;
while (x>=1000 && i<6) { x/=1000; i++ }
if (x<10) printf("%.2f %s", x, u[i]);
else if (x<100)printf("%.1f %s", x, u[i]);
else printf("%.0f %s", x, u[i]);
}'
}
# Add commas to large integers (e.g. 1000000 -> 1,000,000)
fmt_int() {
[[ "$1" =~ ^[0-9]+$ ]] && printf "%'d" "$1" || printf "%s" "$1"
}
if command -v jq >/dev/null 2>&1; then
WORKERS_LINE="No workers detected (point your ASICs at this node)"
else
WORKERS_LINE="Install jq for worker stats."
fi
if [[ -r "$POOL_STATUS" ]]; then
# pool.status is 3 JSON objects, one per line: counts, hashrate, shares
# Slurp into array so we can index [0],[1],[2]
POOL_JSON="$(jq -s '.' "$POOL_STATUS" 2>/dev/null || true)"
if [[ -n "$POOL_JSON" && "$POOL_JSON" != "null" ]]; then
workers="$(jq -r '.[0].Workers // empty' <<<"$POOL_JSON")"
idle="$(jq -r '.[0].idle // empty' <<<"$POOL_JSON")"
disc="$(jq -r '.[0].disconnected // empty' <<<"$POOL_JSON")"
users="$(jq -r '.[0].users // empty' <<<"$POOL_JSON")"
hr1m="$(jq -r '.[1].hashrate1m // empty' <<<"$POOL_JSON")"
hr5m="$(jq -r '.[1].hashrate5m // empty' <<<"$POOL_JSON")"
hr15m="$(jq -r '.[1].hashrate15m // empty' <<<"$POOL_JSON")"
acc="$(jq -r '.[2].accepted // empty' <<<"$POOL_JSON")"
rej="$(jq -r '.[2].rejected // empty' <<<"$POOL_JSON")"
stale="$(jq -r '.[2].stale // empty' <<<"$POOL_JSON")"
dup="$(jq -r '.[2].dup // empty' <<<"$POOL_JSON")"
sps="$(jq -r '.[2].SPS1m // empty' <<<"$POOL_JSON")"
bestshare="$(jq -r '.[2].bestshare // empty' <<<"$POOL_JSON")"
if [[ -n "$workers" && "$workers" != "0" ]]; then
# Compact worker line (single line, console-safe)
# Example: Workers: 3 (idle 1, disc 0) | HR: 1m 1.2 TH/s / 5m 1.1 TH/s / 15m 1.0 TH/s | Shares: acc 120 rej 0 stale 1 dup 0 | SPS 0.03
WORKERS_LINE="Workers: ${workers}"
[[ -n "$idle" || -n "$disc" ]] && WORKERS_LINE+=" (idle ${idle:-0}, disc ${disc:-0})"
WORKERS_LINE+=" " # Extend a little beyond the end of the current text to blank if new output is shorter
[[ -n "$users" ]] && WORKERS_LINE+="\n Users: ${users}"
WORKERS_LINE+=" " # Extend a little beyond the end of the current text to blank if new output is shorter
WORKERS_LINE+="\n Hashrate: 1m $(human_hashrate "$hr1m") / 5m $(human_hashrate "$hr5m") / 15m $(human_hashrate "$hr15m")"
WORKERS_LINE+=" " # Extend a little beyond the end of the current text to blank if new output is shorter
# Shares (show only fields that exist; keep it tight)
WORKERS_LINE+="\n Shares:"
WORKERS_LINE+="\n Accepted: $(fmt_int "${acc:-0}") "
[[ -n "$rej" ]] && WORKERS_LINE+="\n Rejected: $(fmt_int "$rej") "
[[ -n "$stale" ]] && WORKERS_LINE+="\n Stale: $(fmt_int "$stale") "
[[ -n "$dup" ]] && WORKERS_LINE+="\n Duplicate:$(fmt_int "$dup") "
[[ -n "$sps" ]] && WORKERS_LINE+="\n Shares Per Second: $sps "
[[ -n "$bestshare" ]] && WORKERS_LINE+="\n Best Share: $(fmt_int "$bestshare") "
fi
fi
fi
# --- /CKPool worker summary ---
# cpu load # cpu load
if [ -r /proc/loadavg ]; then if [ -r /proc/loadavg ]; then
@ -2812,7 +2889,7 @@ while true; do
INFO="$LAST_INFO" INFO="$LAST_INFO"
fi fi
else else
# success store and reset fail counter # success - store and reset fail counter
LAST_INFO="$INFO" LAST_INFO="$INFO"
CONSEC_FAILS=0 CONSEC_FAILS=0
fi fi
@ -2844,12 +2921,12 @@ while true; do
if [ "$IBD" = "true" ]; then if [ "$IBD" = "true" ]; then
if [ "$(echo "$PROG_PCT < 0.1" | bc -l 2>/dev/null || echo 0)" = "1" ]; then if [ "$(echo "$PROG_PCT < 0.1" | bc -l 2>/dev/null || echo 0)" = "1" ]; then
STATUS_LINE="${YEL}⚠ Bitcoin Core is starting up and loading blockchain data${RST}" STATUS_LINE="${YEL}⚠ Bitcoin Core is starting up and loading blockchain data...${RST}"
else else
STATUS_LINE="${YEL}⚠ Syncing blockchain ${PROG_PCT}%${RST}" STATUS_LINE="${YEL}⚠ Syncing blockchain... ${PROG_PCT}%${RST}"
fi fi
else else
STATUS_LINE="${GRN}✔ Bitcoin Core is ready.${RST}" STATUS_LINE="${GRN}✔ Bitcoin Core is in sync.${RST}"
fi fi
PROGRESS_LINE="Blocks: ${BLOCKS} / ${HEADERS} $( [ "$PRUNED" = "true" ] && echo '(pruned)' )" PROGRESS_LINE="Blocks: ${BLOCKS} / ${HEADERS} $( [ "$PRUNED" = "true" ] && echo '(pruned)' )"