Early dashboard development

This commit is contained in:
2025-11-07 16:48:55 -05:00
parent 28d177d4ea
commit 0e74cd8551
2 changed files with 377 additions and 20 deletions

304
installer
View File

@ -1,7 +1,7 @@
#!/bin/bash
#
# Bitcoin Solo Miner Appliance Installer
# An Open Source Appliance from Robbie Ferguson
# Bitcoin Node for Solo Miners Appliance Installer
# An Open Source Linux Appliance from Robbie Ferguson
# (c) 2025 Robbie Ferguson
set -e
@ -13,6 +13,9 @@ CERT_DIR="/etc/ssl/localcerts"
CKPOOL_USER="ckpool"
CKPOOL_PORT="3333"
BITCOIN_DATA="/var/lib/bitcoind"
BTC_RPC_USER="user"
BTC_RPC_PASS="pass"
PRUNE_SIZE="550"
# ---------------------------------------------------------------------------
# PURGE
@ -99,17 +102,22 @@ EOF
mkdir -p /etc/bitcoin
mkdir -p "${BITCOIN_DATA}"
# temporary admin creds; user will change on first boot
# Bitcoin Core Configuration
cat > "$BITCOIN_CONF" <<EOF
server=1
txindex=0
rpcuser=user
rpcpassword=pass
rpcallowip=127.0.0.1
rpcbind=127.0.0.1
listen=1
prune=550
daemon=0
prune=${PRUNE_SIZE}
rpcuser=${BTC_RPC_USER}
rpcpassword=${BTC_RPC_PASS}
rpcbind=127.0.0.1
maxconnections=250 # Allow up to 250 connections to speed up sync
maxuploadtarget=0 # Unlimited upload speed
dbcache=2048 # 2 GB RAM for sync
maxmempool=300 # Use less RAM post-sync
EOF
systemctl daemon-reload
@ -155,6 +163,16 @@ if [[ ! -x "/opt/btc-solo/ckpool/src/ckpool" ]]; then
fi
id -u "$CKPOOL_USER" >/dev/null 2>&1 || useradd -r -s /bin/false "$CKPOOL_USER"
usermod -aG $CKPOOL_USER www-data
# try to restart the running PHP-FPM service so www-data picks up new group
php_fpm_service=""
# 1) ask systemd what php-fpm services exist
php_fpm_service=$(systemctl list-units --type=service --all \
| awk '/php.*fpm\.service/ {print $1; exit}')
if [ -n "$php_fpm_service" ]; then
systemctl restart "$php_fpm_service"
fi
mkdir -p "$APP_ROOT/conf" "$APP_ROOT/logs"
cat > "$APP_ROOT/conf/ckpool.conf" <<EOF
@ -173,6 +191,13 @@ EOF
chown -R "$CKPOOL_USER":"$CKPOOL_USER" "$APP_ROOT"
# create the socket folder for CKPool so we can later parse the JSON output
mkdir -p /var/run/ckpool
chown ${CKPOOL_USER}:${CKPOOL_USER} /var/run/ckpool
# make ckpmsg available to PHP
ln -sf /opt/btc-solo/ckpool/src/ckpmsg /usr/local/bin/ckpmsg
cat > /etc/systemd/system/ckpool.service <<EOF
[Unit]
Description=ckpool solo mining server
@ -182,7 +207,7 @@ After=network.target bitcoind.service
User=${CKPOOL_USER}
Group=${CKPOOL_USER}
WorkingDirectory=${APP_ROOT}/ckpool
ExecStart=${APP_ROOT}/ckpool/src/ckpool -c ${APP_ROOT}/conf/ckpool.conf -B
ExecStart=${APP_ROOT}/ckpool/src/ckpool -c ${APP_ROOT}/conf/ckpool.conf -B -s /var/run/ckpool
Restart=on-failure
[Install]
@ -321,7 +346,7 @@ chown www-data:www-data /var/lib/btc-solo
mkdir -p "$WWW_ROOT"
mkdir -p "$WWW_ROOT/activate"
# main dashboard
# Main dashboard
cat > "$WWW_ROOT/index.php" <<'EOF'
<?php
$flag = '/var/lib/btc-solo/.setup-complete';
@ -329,6 +354,177 @@ if (!file_exists($flag)) {
header('Location: /activate/');
exit;
}
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_ADDR'] ?? gethostname();
$host = preg_replace('/:\d+$/', '', $host);
$stratum = 'stratum+tcp://' . $host . ':3333';
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Bitcoin Node for Solo Miners</title>
<style>
body { font-family: sans-serif; background: #111; color: #eee; margin: 0; }
header { background: #222; padding: 1rem 2rem; display:flex; justify-content:space-between; align-items:center; }
h1 { margin: 0; }
a { color: #7ad; text-decoration: none; }
.card { background: #1b1b1b; margin: 1rem 2rem; padding: 1rem 1.5rem; border-radius: 12px; }
code { background: #333; padding: 0.25rem 0.5rem; border-radius: 6px; }
.status-ok { color: #8f8; }
.status-warn { color: #ffb347; }
.status-err { color: #ff6b6b; }
.progress-wrap { background:#000; border-radius:6px; height:18px; overflow:hidden; }
.progress-bar { background:#4caf50; height:100%; width:0%; transition:width .4s ease; }
ul.status-list { list-style:none; padding-left:0; }
ul.status-list li { margin-bottom:.25rem; }
</style>
</head>
<body>
<header>
<div>
<h1>Bitcoin Node for Solo Miners</h1>
<p style="margin:0;">A Linux Appliance by Robbie Ferguson</p>
</div>
<div>
<a href="/settings.php">Settings</a>
</div>
</header>
<div class="card" id="node-status">
<h2>Status</h2>
<p>Loading status…</p>
</div>
<div class="card" id="workers">
<h2>Workers</h2>
<p>No workers detected yet. This is normal while Bitcoin Core is syncing or if no ASICs are pointed at this server.</p>
</div>
<script>
// per-tab tracker with sliding window
const syncTracker = { samples: [] };
const WINDOW_SECONDS = 900; // 15 minutes
function formatEta(seconds) {
if (seconds == null || !isFinite(seconds)) return 'estimating…';
if (seconds < 60) return seconds.toFixed(0) + 's';
const mins = seconds / 60;
if (mins < 60) return mins.toFixed(1) + 'm';
const hours = mins / 60;
return hours.toFixed(1) + 'h';
}
function updateSamples(progress) {
const now = Date.now() / 1000;
syncTracker.samples.push({ t: now, p: progress });
const cutoff = now - WINDOW_SECONDS;
syncTracker.samples = syncTracker.samples.filter(s => s.t >= cutoff);
}
function estimateEta(progress) {
const samples = syncTracker.samples;
if (samples.length < 2) return null;
const first = samples[0];
const last = samples[samples.length - 1];
const dP = last.p - first.p;
const dT = last.t - first.t;
if (dP <= 0 || dT <= 5) return null;
const rate = dP / dT; // % per second
if (rate <= 0) return null;
const remaining = Math.max(0, 100 - progress);
return remaining / rate;
}
function renderServices(svcs) {
if (!svcs) return '';
const map = {
bitcoind: 'Bitcoin Core',
ckpool: 'CKPool'
};
let out = '<h3>Service Health</h3><ul class="status-list">';
for (const key of Object.keys(map)) {
if (!(key in svcs)) continue;
const state = svcs[key];
const ok = state === 'active';
out += '<li>' + (ok ? '✅' : '❌') + ' ' + map[key] + ' — ' + state + '</li>';
}
out += '</ul>';
return out;
}
async function loadStatus() {
try {
const res = await fetch('/status.php', {cache: 'no-store'});
const data = await res.json();
const box = document.getElementById('node-status');
if (data.error) {
box.innerHTML = '<h2>Status</h2><p class="status-err">' + data.error + '</p>' + renderServices(data.services);
return;
}
const ibd = !!data.initialblockdownload;
const blocks = data.blocks ?? '';
const pruned = data.pruned ? '(pruned)' : '';
let progress = 0;
if (typeof data.verificationprogress === 'number') {
progress = data.verificationprogress * 100;
} else if (data.blocks && data.headers && data.headers > 0) {
progress = (data.blocks / data.headers) * 100;
}
// update window + estimate
updateSamples(progress);
const etaSeconds = ibd ? estimateEta(progress) : null;
let html = '<h2>Status</h2>';
if (ibd) {
html += '<p class="status-warn">⚠ Bitcoin Core is syncing. Your miners will not be able to submit shares or receive work until the node is fully synced.</p>';
} else {
html += '<p class="status-ok">✔ Bitcoin Core is in sync.</p>';
}
html += '<p>Blocks: ' + blocks + ' ' + pruned + '</p>';
html += '<div class="progress-wrap"><div class="progress-bar" id="syncbar"></div></div>';
html += '<p><small>Sync progress: ' + progress.toFixed(2) + '%';
if (ibd) {
html += ' • Estimated time left: ' + formatEta(etaSeconds);
}
html += '</small></p>';
// append service health
html += renderServices(data.services);
box.innerHTML = html;
const bar = document.getElementById('syncbar');
if (bar) {
bar.style.width = Math.min(progress, 100).toFixed(2) + '%';
}
} catch (e) {
const box = document.getElementById('node-status');
box.innerHTML = '<h2>Status</h2><p class="status-err">Failed to fetch status.</p>';
}
}
loadStatus();
setInterval(loadStatus, 5000);
</script>
</body>
</html>
EOF
# Settings page
cat > "$WWW_ROOT/settings.php" <<'EOF'
<?php
$flag = '/var/lib/btc-solo/.setup-complete';
if (!file_exists($flag)) {
header('Location: /activate/');
exit;
}
$opts = [
'550' => '550 MB (default, small)',
@ -370,7 +566,7 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
<html>
<head>
<meta charset="utf-8">
<title>Local Bitcoin Solo Mining</title>
<title>Bitcoin Node for Solo Miners [Settings]</title>
<style>
body { font-family: sans-serif; background: #111; color: #eee; margin: 0; }
header { background: #222; padding: 1rem 2rem; }
@ -384,17 +580,17 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
</head>
<body>
<header>
<h1>Local Bitcoin Solo Mining</h1>
<p>Point your ASICs to this server and keep 100% of your block.</p>
<h1>Bitcoin Node for Solo Miners</h1>
<p style="margin:0;">A Linux Appliance by Robbie Ferguson</p>
</header>
<div class="card">
<h2>Stratum Endpoint</h2>
<p><code><?php echo htmlspecialchars($stratum); ?></code></p>
<p>Username: your BTC address</p>
<p>Username: BTC address for payout</p>
<p>Password: anything</p>
</div>
<div class="card">
<h2>Prune Size</h2>
<h2>Blockchain Prune Size</h2>
<form method="post">
<label for="prune">Current setting:</label>
<select name="prune" id="prune">
@ -410,11 +606,11 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
</form>
</div>
<div class="card">
<h2>Operator / Donation</h2>
<p><small>Default is 2% to the project address. Set to 0 to keep the full block.</small></p>
<h2>Donate to Bald Nerd</h2>
<p><small>If you happen to win a BTC block, you can donate a small percentage to Robbie Ferguson (developer of this appliance, host of Category5 Technology TV) with big thanks. You can set to 0 if you prefer to keep the full block to yourself.</small></p>
<?php
$conf = @file_get_contents('/opt/btc-solo/conf/ckpool.conf');
$currAddr = 'YOUR_HARDCODED_DONATION_ADDRESS';
$currAddr = '1MoGAsK8bRFKjHrpnpFJyqxdqamSPH19dP';
$currRate = 2;
if ($conf) {
if (preg_match('/"donaddress"\s*:\s*"([^"]+)"/', $conf, $m)) $currAddr = $m[1];
@ -445,14 +641,58 @@ $stratum = 'stratum+tcp://' . $host . ':3333';
<button type="submit">Save</button>
</form>
</div>
<div class="card">
<h2>System</h2>
<p>Check <code>systemctl status bitcoind</code> and <code>systemctl status ckpool</code>.</p>
</div>
</body>
</html>
EOF
# AJAX status info
cat > "$WWW_ROOT/status.php" <<'EOF'
<?php
// no session needed; this is polled often
$flag = '/var/lib/btc-solo/.setup-complete';
if (!file_exists($flag)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['error' => 'not activated']);
exit;
}
// Bitcoin Core status
$cmd = '/usr/local/bin/bitcoin-cli -conf=/etc/bitcoin/bitcoin.conf getblockchaininfo 2>/dev/null';
$out = shell_exec($cmd);
$info = $out ? json_decode($out, true) : null;
// service health (simple is-active check)
function svc($name) {
$res = trim(shell_exec('systemctl is-active ' . escapeshellarg($name) . ' 2>/dev/null'));
return $res === 'active' ? 'active' : $res;
}
$services = [
'bitcoind' => svc('bitcoind'),
'ckpool' => svc('ckpool')
];
header('Content-Type: application/json');
if (!$info) {
echo json_encode([
'error' => 'bitcoin-cli failed',
'services' => $services
]);
exit;
}
echo json_encode([
'initialblockdownload' => $info['initialblockdownload'] ?? null,
'blocks' => $info['blocks'] ?? null,
'headers' => $info['headers'] ?? null,
'verificationprogress' => $info['verificationprogress'] ?? null,
'pruned' => $info['pruned'] ?? null,
'services' => $services
]);
EOF
# activation page
cat > "$WWW_ROOT/activate/index.php" <<'EOF'
<?php
@ -574,6 +814,30 @@ rm -f /etc/nginx/sites-enabled/default || true
systemctl restart nginx
echo "Configuring Log Rotation"
cat > /etc/logrotate.d/btc-solo <<'EOF'
/opt/btc-solo/logs/*.log {
weekly
rotate 8
compress
missingok
notifempty
copytruncate
}
EOF
cat > /etc/logrotate.d/nginx <<'EOF'
/var/log/nginx/*.log {
weekly
rotate 8
compress
missingok
notifempty
copytruncate
}
EOF
# ---------------------------------------------------------------------------
echo "[8/8] Done"
# ---------------------------------------------------------------------------