git / moonbite-network / templates/mining.html · raw
{% extends "base.html" %} {% block body_class %}mb-v2{% endblock %} {% block title %}Mining - MoonBite Dashboard{% endblock %} {% block nav_active %}mining{% endblock %} {% block head_extra %} <link rel="stylesheet" href="{{ url_for('static', filename='mining-god.css') }}"> {% endblock %} {% block content %} <div class="main-content"> <section class="hero"> <h2>Mining</h2> <p>Mine blocks and earn coinbase rewards to your address.</p> </section> <!-- Mining Controls --> <div class="card card-full"> <div class="card-header">Mining Configuration</div> <div class="card-body"> <form id="miningForm" class="mining-form"> <div class="form-group"> <label for="blocksInput">Number of Blocks to Mine</label> <input type="number" id="blocksInput" name="blocks" min="1" max="100" value="1" class="form-input" > </div> <div class="form-group"> <label for="addressInput">Miner Address (Base58)</label> <input type="text" id="addressInput" name="address" placeholder="Enter your mining address..." class="form-input" > <small>Don't have an address? <a href="/wallet" class="link">Generate one</a></small> </div> <div class="form-actions"> <button type="submit" id="startBtn" class="btn btn-success btn-large"> Start Mining </button> <button type="button" id="stopBtn" class="btn btn-danger btn-large hidden" data-act="stop-mining"> Stop Mining </button> </div> </form> </div> </div> <!-- Mining Progress — GOD-TIER cinematic reactor --> <div id="progressSection" class="card card-full hidden"> <div class="card-header">Live Mining Reactor</div> <div class="card-body"> <!-- Cinematic stage: canvases + HUD (mining-god.js drives it) --> <div id="godStage" class="god-stage"> <div class="god-grid"></div> <div class="god-aurora"></div> <canvas id="godParticles" class="god-canvas"></canvas> <canvas id="godReactor" class="god-canvas"></canvas> <div class="god-scan"></div> <div class="god-badge"><span></span>MOONBITE RIG</div> <div class="god-spark"> <div class="god-spark-title">Hashrate · live</div> <canvas id="godSpark"></canvas> </div> <div class="god-core-readout"> <div id="godHashrate" class="god-hashrate">0 H/s</div> <div class="god-hashunit">Proof of Work</div> <div class="god-state"> <span class="god-dot"></span> <span id="godStateText">STANDBY</span> </div> </div> <div class="god-beam-wrap"> <div class="god-beam-labels"> <span>BLOCK YIELD</span> <span><b id="godPct">0%</b></span> </div> <div class="god-beam"> <div id="godBeamFill" class="god-beam-fill"></div> </div> </div> <div class="god-hud"> <div class="god-hud-cell"> <div class="god-hud-label">Chain Height</div> <div id="godHeight" class="god-hud-value">--</div> </div> <div class="god-hud-cell"> <div class="god-hud-label">Blocks</div> <div id="godBlocks" class="god-hud-value">0 / 0</div> </div> <div class="god-hud-cell"> <div class="god-hud-label">Elapsed</div> <div id="godElapsed" class="god-hud-value">0s</div> </div> </div> </div> <!-- Legacy state carriers: kept for the existing poll logic to write into. Visually hidden; mining-god.js reads them. --> <div class="god-legacy" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;"> <div id="miningOrb" class="mining-orb idle"> <div class="orb-ring"></div> <div class="orb-ring ring-2"></div> <div class="orb-core"></div> </div> <span id="blocksMined">0</span> <span id="totalBlocks">0</span> <div id="progressBarFill" class="progress-fill" style="width: 0%"></div> <div id="currentHeight">--</div> <code id="currentHash">--</code> <div id="elapsedTime">0s</div> </div> </div> </div> <!-- Miner badges (earned locally, judged by first-block chain height) --> <div class="card card-full"> <div class="card-header"> Your Miner Badges</div> <div class="card-body"> <div id="mbBadgeShelf" class="mb-badge-shelf"></div> <div class="card-subtitle" style="margin-top:10px;">Earned by mining, never bought. Height tiers are locked to the chain height of your first block — being early can't be copied.</div> </div> </div> <!-- Mining Status Display --> <div class="cards-grid"> <div class="card"> <div class="card-header">Mining Status</div> <div class="card-body"> <div id="miningStatusDisplay" class="card-value">Idle</div> <div class="card-subtitle">Current state</div> </div> </div> <div class="card"> <div class="card-header">Chain Height</div> <div class="card-body"> <div id="chainHeightDisplay" class="card-value">--</div> <div class="card-subtitle">Latest block height</div> </div> </div> <div class="card"> <div class="card-header">Blocks This Session</div> <div class="card-body"> <div id="sessionBlocksDisplay" class="card-value">0</div> <div class="card-subtitle">Total mined</div> </div> </div> </div> <!-- Information Box --> <div class="card card-full"> <div class="card-header">Mining Information</div> <div class="card-body"> <ul class="info-list"> <li>Mining uses Proof-of-Work consensus to secure the blockchain</li> <li>Each successfully mined block earns a coinbase reward to your address</li> <li>The difficulty adjusts automatically based on block times</li> <li>You can stop mining at any time using the "Stop Mining" button</li> <li>Mining progress updates in real-time on this page</li> </ul> </div> </div> <!-- Status Messages --> <div id="statusMessage" class="status-message hidden"></div> </div> {% endblock %} {% block scripts_extra %} <script nonce="{{ csp_nonce }}"> let isMining = false; let startTime = 0; let statusCheckInterval = null; function showStatus(message, type = 'info') { const statusEl = document.getElementById('statusMessage'); statusEl.textContent = message; statusEl.className = `status-message ${type}`; statusEl.classList.remove('hidden'); setTimeout(() => { statusEl.classList.add('hidden'); }, 5000); } async function startMining(event) { event.preventDefault(); const blocksInput = document.getElementById('blocksInput').value; const addressInput = document.getElementById('addressInput').value; if (!blocksInput || blocksInput < 1) { showStatus('Please enter a valid number of blocks', 'error'); return; } if (!addressInput) { showStatus('Please enter your mining address', 'error'); return; } const startBtn = document.getElementById('startBtn'); startBtn.disabled = true; startBtn.textContent = 'Starting...'; try { const response = await fetch('/api/mining/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ blocks: parseInt(blocksInput), address: addressInput, }), }); const data = await response.json(); if (response.ok && data.status === 'mining') { isMining = true; startTime = Date.now(); document.getElementById('miningForm').style.display = 'none'; document.getElementById('progressSection').classList.remove('hidden'); document.getElementById('startBtn').classList.add('hidden'); document.getElementById('stopBtn').classList.remove('hidden'); document.getElementById('totalBlocks').textContent = data.blocks_to_mine; document.getElementById('miningOrb').classList.remove('idle'); showStatus(' Mining started', 'success'); // Poll for mining status every 500ms statusCheckInterval = setInterval(pollMiningStatus, 500); } else { showStatus('Error: ' + (data.message || 'Failed to start mining'), 'error'); startBtn.disabled = false; startBtn.textContent = ' Start Mining'; } } catch (error) { console.error('Error:', error); showStatus('Network error: ' + error.message, 'error'); startBtn.disabled = false; startBtn.textContent = ' Start Mining'; } } async function pollMiningStatus() { try { const response = await fetch('/api/mining/status'); const data = await response.json(); if (data.status !== 'error') { const blocksMined = data.blocks_mined; const totalBlocks = data.total_blocks; const currentHeight = data.current_height; // Update progress bar const progress = totalBlocks > 0 ? (blocksMined / totalBlocks) * 100 : 0; document.getElementById('progressBarFill').style.width = progress + '%'; document.getElementById('blocksMined').textContent = blocksMined; document.getElementById('currentHeight').textContent = currentHeight; document.getElementById('chainHeightDisplay').textContent = currentHeight; document.getElementById('sessionBlocksDisplay').textContent = blocksMined; // Badges + first-block celebration (additive; never throws) if (window.MoonBiteBadges) { try { MoonBiteBadges.recordBlocks(blocksMined, currentHeight, document.getElementById('addressInput').value.trim()); } catch (e) { console.error('badge error:', e); } } // Update elapsed time const elapsed = Math.floor((Date.now() - startTime) / 1000); const minutes = Math.floor(elapsed / 60); const seconds = elapsed % 60; const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; document.getElementById('elapsedTime').textContent = timeStr; // Check if mining is complete if (data.status !== 'mining' || blocksMined >= totalBlocks) { stopMiningUI(); } else { document.getElementById('miningStatusDisplay').textContent = 'Mining...'; } } } catch (error) { console.error('Error polling status:', error); } } async function stopMining() { try { const response = await fetch('/api/mining/stop'); const data = await response.json(); if (data.status === 'stopped') { stopMiningUI(); showStatus(' Mining stopped', 'success'); } } catch (error) { console.error('Error stopping mining:', error); showStatus('Error stopping mining', 'error'); } } function stopMiningUI() { isMining = false; if (statusCheckInterval) { clearInterval(statusCheckInterval); } document.getElementById('miningStatusDisplay').textContent = 'Idle'; document.getElementById('miningOrb').classList.add('idle'); document.getElementById('progressSection').classList.add('hidden'); document.getElementById('miningForm').style.display = 'block'; document.getElementById('startBtn').classList.remove('hidden'); document.getElementById('stopBtn').classList.add('hidden'); document.getElementById('startBtn').disabled = false; document.getElementById('startBtn').textContent = ' Start Mining'; } // Populate address field if available from wallet document.addEventListener('DOMContentLoaded', () => { const savedAddresses = JSON.parse(sessionStorage.getItem('addresses') || '[]'); if (savedAddresses.length > 0) { document.getElementById('addressInput').value = savedAddresses[0].address; } // Paint the badge shelf for the current address (greyed until earned) if (window.MoonBiteBadges) { MoonBiteBadges.renderShelf('mbBadgeShelf', document.getElementById('addressInput').value.trim()); document.getElementById('addressInput').addEventListener('input', function () { MoonBiteBadges.renderShelf('mbBadgeShelf', this.value.trim()); }); } // Poll blockchain info in background setInterval(() => { if (!isMining) { fetch('/api/blockchain/info') .then(r => r.json()) .then(data => { if (data.status === 'success') { document.getElementById('chainHeightDisplay').textContent = data.height; } }) .catch(e => console.error('Error:', e)); } }, 2000); }); // Handle form submission document.getElementById('miningForm').addEventListener('submit', startMining); </script> <script src="{{ url_for('static', filename='moonbite-badges.js') }}"></script> <script defer src="{{ url_for('static', filename='mining-god.js') }}"> document.addEventListener('click', function (e) { // Delegated so the page carries no inline handlers and can run under a // strict CSP (no 'unsafe-inline'). var el = e.target.closest && e.target.closest('[data-act="stop-mining"]'); if (el) stopMining(); }); </script> {% endblock %}