Early dashboard development
This commit is contained in:
304
installer
304
installer
@ -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"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user