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 %}