| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>XMRig Mining Server</title>
|
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| <style>
|
| * {
|
| margin: 0;
|
| padding: 0;
|
| box-sizing: border-box;
|
| }
|
|
|
| body {
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| min-height: 100vh;
|
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| padding: 20px;
|
| }
|
|
|
| .container-main {
|
| max-width: 1000px;
|
| margin: 0 auto;
|
| }
|
|
|
| .header {
|
| text-align: center;
|
| color: white;
|
| margin-bottom: 40px;
|
| animation: slideDown 0.6s ease-out;
|
| }
|
|
|
| @keyframes slideDown {
|
| from {
|
| opacity: 0;
|
| transform: translateY(-30px);
|
| }
|
| to {
|
| opacity: 1;
|
| transform: translateY(0);
|
| }
|
| }
|
|
|
| .header h1 {
|
| font-size: 3em;
|
| font-weight: 700;
|
| margin-bottom: 10px;
|
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
| }
|
|
|
| .header p {
|
| font-size: 1.2em;
|
| opacity: 0.95;
|
| }
|
|
|
| .card-custom {
|
| background: rgba(255, 255, 255, 0.95);
|
| border: none;
|
| border-radius: 15px;
|
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
| backdrop-filter: blur(10px);
|
| margin-bottom: 25px;
|
| animation: fadeIn 0.6s ease-out;
|
| }
|
|
|
| @keyframes fadeIn {
|
| from {
|
| opacity: 0;
|
| transform: translateY(20px);
|
| }
|
| to {
|
| opacity: 1;
|
| transform: translateY(0);
|
| }
|
| }
|
|
|
| .card-header-custom {
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| color: white;
|
| font-weight: 600;
|
| font-size: 1.2em;
|
| padding: 20px;
|
| border-radius: 15px 15px 0 0;
|
| display: flex;
|
| align-items: center;
|
| gap: 10px;
|
| }
|
|
|
| .card-body-custom {
|
| padding: 25px;
|
| }
|
|
|
| .status-badge {
|
| display: inline-block;
|
| padding: 10px 20px;
|
| border-radius: 25px;
|
| font-weight: 600;
|
| font-size: 0.95em;
|
| }
|
|
|
| .status-running {
|
| background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
| color: white;
|
| animation: pulse 1.5s infinite;
|
| }
|
|
|
| @keyframes pulse {
|
| 0%, 100% { opacity: 1; }
|
| 50% { opacity: 0.7; }
|
| }
|
|
|
| .status-stopped {
|
| background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
|
| color: white;
|
| }
|
|
|
| .status-admin {
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| color: white;
|
| }
|
|
|
| .info-row {
|
| display: grid;
|
| grid-template-columns: 1fr 1fr;
|
| gap: 20px;
|
| margin-bottom: 15px;
|
| }
|
|
|
| .info-item {
|
| padding: 15px;
|
| background: #f8f9fa;
|
| border-radius: 10px;
|
| border-left: 4px solid #667eea;
|
| }
|
|
|
| .info-item label {
|
| font-weight: 600;
|
| color: #333;
|
| display: block;
|
| margin-bottom: 5px;
|
| }
|
|
|
| .info-item value {
|
| font-size: 1.1em;
|
| color: #667eea;
|
| font-weight: 600;
|
| }
|
|
|
| .form-group {
|
| margin-bottom: 20px;
|
| }
|
|
|
| .form-group label {
|
| font-weight: 600;
|
| color: #333;
|
| margin-bottom: 8px;
|
| display: block;
|
| }
|
|
|
| .form-control-custom {
|
| border: 2px solid #e0e0e0;
|
| border-radius: 10px;
|
| padding: 12px 15px;
|
| font-size: 1em;
|
| transition: all 0.3s ease;
|
| }
|
|
|
| .form-control-custom:focus {
|
| border-color: #667eea;
|
| box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.1);
|
| outline: none;
|
| }
|
|
|
| .btn-custom {
|
| font-weight: 600;
|
| padding: 12px 25px;
|
| border-radius: 10px;
|
| border: none;
|
| transition: all 0.3s ease;
|
| font-size: 1em;
|
| }
|
|
|
| .btn-start {
|
| background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
| color: white;
|
| }
|
|
|
| .btn-start:hover {
|
| transform: translateY(-2px);
|
| box-shadow: 0 10px 25px rgba(17, 153, 142, 0.3);
|
| color: white;
|
| }
|
|
|
| .btn-start:disabled {
|
| opacity: 0.6;
|
| cursor: not-allowed;
|
| }
|
|
|
| .btn-stop {
|
| background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
|
| color: white;
|
| }
|
|
|
| .btn-stop:hover {
|
| transform: translateY(-2px);
|
| box-shadow: 0 10px 25px rgba(235, 51, 73, 0.3);
|
| color: white;
|
| }
|
|
|
| .btn-stop:disabled {
|
| opacity: 0.5;
|
| cursor: not-allowed;
|
| }
|
|
|
| .btn-group-custom {
|
| display: grid;
|
| grid-template-columns: 1fr 1fr;
|
| gap: 15px;
|
| margin-top: 20px;
|
| }
|
|
|
| .logs-container {
|
| background: #1e1e1e;
|
| color: #0fc700;
|
| padding: 20px;
|
| border-radius: 10px;
|
| font-family: 'Courier New', monospace;
|
| font-size: 0.9em;
|
| line-height: 1.6;
|
| max-height: 500px;
|
| overflow-y: auto;
|
| border: 2px solid #333;
|
| }
|
|
|
| .logs-container::-webkit-scrollbar {
|
| width: 8px;
|
| }
|
|
|
| .logs-container::-webkit-scrollbar-track {
|
| background: #2d2d2d;
|
| border-radius: 5px;
|
| }
|
|
|
| .logs-container::-webkit-scrollbar-thumb {
|
| background: #667eea;
|
| border-radius: 5px;
|
| }
|
|
|
| .log-line {
|
| margin: 2px 0;
|
| word-break: break-word;
|
| }
|
|
|
| .log-error {
|
| color: #ff6b6b;
|
| }
|
|
|
| .log-success {
|
| color: #51cf66;
|
| }
|
|
|
| .log-warning {
|
| color: #ffd700;
|
| }
|
|
|
| .log-info {
|
| color: #74c0fc;
|
| }
|
|
|
| .spinner {
|
| display: none;
|
| width: 18px;
|
| height: 18px;
|
| border: 3px solid rgba(255, 255, 255, 0.3);
|
| border-top-color: white;
|
| border-radius: 50%;
|
| animation: spin 0.8s linear infinite;
|
| margin-right: 8px;
|
| }
|
|
|
| @keyframes spin {
|
| to { transform: rotate(360deg); }
|
| }
|
|
|
| .spinner.active {
|
| display: inline-block;
|
| }
|
|
|
| .stats-grid {
|
| display: grid;
|
| grid-template-columns: 1fr 1fr;
|
| gap: 15px;
|
| }
|
|
|
| .stat-box {
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| color: white;
|
| padding: 20px;
|
| border-radius: 10px;
|
| text-align: center;
|
| }
|
|
|
| .stat-label {
|
| font-size: 0.9em;
|
| opacity: 0.9;
|
| margin-bottom: 5px;
|
| }
|
|
|
| .stat-value {
|
| font-size: 1.8em;
|
| font-weight: 700;
|
| }
|
|
|
| .warning-box {
|
| background: #fff3cd;
|
| border: 2px solid #ffc107;
|
| border-radius: 10px;
|
| padding: 15px;
|
| margin-bottom: 20px;
|
| display: none;
|
| }
|
|
|
| .warning-box.show {
|
| display: block;
|
| }
|
|
|
| .warning-box strong {
|
| color: #856404;
|
| }
|
|
|
| @media (max-width: 768px) {
|
| .info-row, .stats-grid {
|
| grid-template-columns: 1fr;
|
| }
|
|
|
| .btn-group-custom {
|
| grid-template-columns: 1fr;
|
| }
|
|
|
| .header h1 {
|
| font-size: 2em;
|
| }
|
| }
|
| </style>
|
| </head>
|
| <body>
|
| <div class="container-main">
|
|
|
| <div class="header">
|
| <h1>⛏️ XMRig Mining</h1>
|
| <p>RandomX Benchmarking Server</p>
|
| </div>
|
|
|
|
|
| <div class="card-custom">
|
| <div class="card-header-custom">
|
| 📊 System Status
|
| </div>
|
| <div class="card-body-custom">
|
| <div class="stats-grid">
|
| <div class="stat-box">
|
| <div class="stat-label">Platform</div>
|
| <div class="stat-value" id="platform">Loading...</div>
|
| </div>
|
| <div class="stat-box">
|
| <div class="stat-label">CPU Cores</div>
|
| <div class="stat-value" id="cpu-count">Loading...</div>
|
| </div>
|
| </div>
|
| <div style="margin-top: 15px;">
|
| <div id="admin-status" style="text-align: center;"></div>
|
| <div style="text-align: center; margin-top: 10px; color: #666;">
|
| <strong>XMRig:</strong> <span id="binary-status">Loading...</span>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="card-custom">
|
| <div class="card-header-custom">
|
| 🚀 Mining Control
|
| </div>
|
| <div class="card-body-custom">
|
| <div style="text-align: center; margin-bottom: 20px;">
|
| <span id="status-badge" class="status-badge status-stopped">⏹️ Stopped</span>
|
| </div>
|
|
|
| <div class="warning-box" id="admin-warning">
|
| <strong>⚠️ Warning:</strong> Not running as Administrator. Performance will be limited. Run as Admin for 2-3x better hash rates.
|
| </div>
|
|
|
|
|
| <div class="form-group">
|
| <label>Benchmark Type</label>
|
| <select id="benchmark-type" class="form-control-custom w-100">
|
| <option value="1M">1M Hash (Fast ~1 min)</option>
|
| <option value="10M" selected>10M Hash (Accurate ~10 min)</option>
|
| </select>
|
| <small style="color: #999; display: block; margin-top: 5px;">Choose benchmark size</small>
|
| </div>
|
|
|
|
|
| <div class="form-group">
|
| <label>Threads</label>
|
| <input type="number" id="threads" class="form-control-custom w-100" value="8" min="1" max="64">
|
| <small style="color: #999; display: block; margin-top: 5px;">Number of threads (default: 8)</small>
|
| </div>
|
|
|
|
|
| <div class="btn-group-custom">
|
| <button class="btn btn-custom btn-start" id="start-btn" onclick="startMining()">
|
| <span class="spinner" id="start-spinner"></span>
|
| ▶️ Start Mining
|
| </button>
|
| <button class="btn btn-custom btn-stop" id="stop-btn" onclick="stopMining()" disabled>
|
| ⏹️ Stop Mining
|
| </button>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div class="card-custom">
|
| <div class="card-header-custom">
|
| 📋 Live Mining Logs
|
| </div>
|
| <div class="card-body-custom">
|
| <div class="logs-container" id="logs-container">
|
| <div class="log-line log-info">[*] Waiting for mining to start...</div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| <script>
|
| const API_URL = '/';
|
| let statusCheckInterval = null;
|
| let lastLogCount = 0;
|
|
|
| async function getStatus() {
|
| try {
|
| const response = await fetch(API_URL + 'status');
|
| const data = await response.json();
|
| updateUI(data);
|
| } catch (error) {
|
| console.error('Error fetching status:', error);
|
| }
|
| }
|
|
|
| async function getLogs() {
|
| try {
|
| const response = await fetch(API_URL + 'logs');
|
| const data = await response.json();
|
| updateLogs(data.logs);
|
| } catch (error) {
|
| console.error('Error fetching logs:', error);
|
| }
|
| }
|
|
|
| function updateLogs(logs) {
|
| const logsContainer = document.getElementById('logs-container');
|
|
|
| if (logs.length === 0) {
|
| logsContainer.innerHTML = '<div class="log-line log-info">[*] Waiting for logs...</div>';
|
| return;
|
| }
|
|
|
| if (logs.length > lastLogCount) {
|
| const newLogs = logs.slice(lastLogCount);
|
| newLogs.forEach(log => {
|
| const logLine = document.createElement('div');
|
| logLine.className = 'log-line';
|
|
|
| if (log.message.includes('[!]') || log.message.includes('FAILED')) {
|
| logLine.classList.add('log-error');
|
| } else if (log.message.includes('[+]')) {
|
| logLine.classList.add('log-success');
|
| } else if (log.message.includes('[*]')) {
|
| logLine.classList.add('log-info');
|
| } else if (log.message.includes('H/s') || log.message.includes('speed')) {
|
| logLine.classList.add('log-success');
|
| } else {
|
| logLine.classList.add('log-info');
|
| }
|
|
|
| logLine.textContent = log.message;
|
| logsContainer.appendChild(logLine);
|
| });
|
|
|
| logsContainer.scrollTop = logsContainer.scrollHeight;
|
| }
|
|
|
| lastLogCount = logs.length;
|
| }
|
|
|
| function updateUI(data) {
|
|
|
| document.getElementById('platform').textContent = data.system.platform;
|
| document.getElementById('cpu-count').textContent = data.system.cpu_count;
|
|
|
|
|
| const adminStatus = document.getElementById('admin-status');
|
| if (data.system.admin) {
|
| adminStatus.innerHTML = '<span class="status-badge status-admin">✓ Administrator</span>';
|
| document.getElementById('admin-warning').classList.remove('show');
|
| } else {
|
| adminStatus.innerHTML = '<span class="status-badge" style="background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%); color: white;">✗ Standard User</span>';
|
| document.getElementById('admin-warning').classList.add('show');
|
| }
|
|
|
|
|
| if (data.xmrig.binary_exists) {
|
| document.getElementById('binary-status').textContent = '✓ Ready';
|
| document.getElementById('start-btn').disabled = false;
|
| } else {
|
| document.getElementById('binary-status').textContent = '✗ Missing';
|
| document.getElementById('start-btn').disabled = true;
|
| }
|
|
|
|
|
| if (data.benchmark.running) {
|
| document.getElementById('status-badge').textContent = '▶️ Running';
|
| document.getElementById('status-badge').className = 'status-badge status-running';
|
| document.getElementById('start-btn').disabled = true;
|
| document.getElementById('stop-btn').disabled = false;
|
| } else {
|
| document.getElementById('status-badge').textContent = '⏹️ Stopped';
|
| document.getElementById('status-badge').className = 'status-badge status-stopped';
|
| document.getElementById('start-btn').disabled = false;
|
| document.getElementById('stop-btn').disabled = true;
|
| }
|
| }
|
|
|
| async function startMining() {
|
| const benchmark = document.getElementById('benchmark-type').value;
|
| const threads = parseInt(document.getElementById('threads').value) || 8;
|
| const btn = document.getElementById('start-btn');
|
| const spinner = document.getElementById('start-spinner');
|
|
|
| btn.disabled = true;
|
| spinner.classList.add('active');
|
|
|
| try {
|
| const response = await fetch(API_URL + 'mining/start', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ benchmark, threads })
|
| });
|
|
|
| if (response.ok) {
|
| const data = await response.json();
|
| console.log('Mining started:', data);
|
| lastLogCount = 0;
|
| startStatusPolling();
|
| await getStatus();
|
| } else {
|
| const error = await response.json();
|
| alert('Error: ' + error.detail);
|
| btn.disabled = false;
|
| spinner.classList.remove('active');
|
| }
|
| } catch (error) {
|
| alert('Error: ' + error.message);
|
| btn.disabled = false;
|
| spinner.classList.remove('active');
|
| }
|
| }
|
|
|
| async function stopMining() {
|
| const btn = document.getElementById('stop-btn');
|
| btn.disabled = true;
|
|
|
| try {
|
| const response = await fetch(API_URL + 'mining/stop', { method: 'POST' });
|
| if (response.ok) {
|
| stopStatusPolling();
|
| await getStatus();
|
| } else {
|
| alert('Failed to stop mining');
|
| }
|
| } catch (error) {
|
| alert('Error: ' + error.message);
|
| }
|
| }
|
|
|
| function startStatusPolling() {
|
| if (statusCheckInterval) return;
|
| statusCheckInterval = setInterval(() => {
|
| getStatus();
|
| getLogs();
|
| }, 1000);
|
| }
|
|
|
| function stopStatusPolling() {
|
| if (statusCheckInterval) {
|
| clearInterval(statusCheckInterval);
|
| statusCheckInterval = null;
|
| }
|
| }
|
|
|
|
|
| getStatus();
|
| getLogs();
|
|
|
|
|
| setInterval(() => {
|
| getStatus();
|
| getLogs();
|
| }, 2000);
|
| </script>
|
| </body>
|
| </html>
|
|
|