/*
* SysDeck - Mining Dashboard Panel (v0.0.34)
* Author: Jeremy Anderson (https://dcos.net)
*
* v0.0.34 EXPANDED TO 1999 POWER-TOOL STYLE. Per user directive:
* "themes and mining they need to be expanded for maximum ui
* control. think 1999 power tool style here." The Mining Dashboard
* panel surfaces every XMRig REST API knob:
*
* - Summary stats total hashrate, pool URL, uptime.
* - Per-thread hashrate table of thread index → hashrate, with
* pause/resume buttons per worker.
* - Pool configuration form to set pool URL / username /
* password via PUT /1/config.
* - Thread configuration number input for thread count, write via
* PUT /1/config.
* - Algorithm picker select dropdown with 7 algorithm presets
* (RandomX, RandomWOW, RandomARQ, etc.)
* - Service controls start / stop / restart xmrig.service via
* systemctl under the cockpit superuser
* channel (polkit org.sysdeck.system.modify).
* - Pause / resume all instant pause/resume of all workers
* via XMRig JSON-RPC.
*
* Mutating ops run via the cockpit superuser channel. No `sudo`
* shell-out from JS. XMRig is GPL-3.0 licensed by the XMRig project.
*/
export async function mount(panel, { bridge, EventBus }) {
panel.innerHTML = renderSkeleton();
// Fire all the read-only calls in parallel.
const [summary, threads, poolCfg, threadsCfg, algoCfg, svcStatus] = await Promise.all([
safe(bridge.mining.workers(), null),
safe(bridge.mining.threads(), {}),
safe(bridge.mining.poolConfigGet(), {}),
safe(bridge.mining.threadsConfigGet(), {}),
safe(bridge.mining.algorithmGet(), {}),
safe(bridge.mining.serviceStatus(), {}),
]);
if (!summary) {
panel.innerHTML = renderXmrigUnreachable(svcStatus);
wireServiceControls(panel, { bridge, EventBus });
return;
}
const hashrate = summary.hashrate?.total?.[0] ?? 0;
const poolUrl = summary.pool?.url ?? '—';
const uptime = summary.uptime ?? 0;
panel.innerHTML = `
${renderSummaryStats(hashrate, poolUrl, uptime, summary)}
${renderServiceControls(svcStatus)}
${renderAllControls()}
${renderPerThreadTable(threads)}
${renderPoolConfigForm(poolCfg)}
${renderThreadsConfigForm(threadsCfg)}
${renderAlgorithmPicker(algoCfg)}
`;
wireEvents(panel, { bridge, EventBus });
EventBus.emit('mining.loaded', { hashrate, threadCount: threads.thread_count });
}
// ── Summary stats ───────────────────────────────────────────────────
function renderSummaryStats(hashrate, poolUrl, uptime, summary) {
return `
Total Hashrate
${formatHashrate(hashrate).split(' ')[0]} ${formatHashrate(hashrate).split(' ').slice(1).join(' ')}
Pool
${escapeHtml(poolUrl)}
Uptime
${formatUptime(uptime)}
${summary.results?.times_full ?? 0} accepted shares · ${summary.results?.times_rejected ?? 0} rejected ·
pool latency ${summary.results?.best_time ?? '—'}s ·
diff ${summary.results?.diff_current ?? '—'}
`;
}
// ── Service controls ────────────────────────────────────────────────
function renderServiceControls(svcStatus) {
const active = svcStatus?.active === true;
return `
▶ Start
■ Stop
↻ Restart
↻ Refresh
Start / Stop / Restart run systemctl on xmrig.service via the cockpit
superuser channel (polkit org.sysdeck.system.modify). No sudo shell-out.
`;
}
// ── All-workers controls (pause / resume) ──────────────────────────
function renderAllControls() {
return `
⏸ Pause all
▶ Resume all
Pause/Resume calls the XMRig JSON-RPC paused / resumed methods — no
service restart required. The workers stop hashing immediately and resume on the same pool.
`;
}
// ── Per-thread hashrate table ───────────────────────────────────────
function renderPerThreadTable(threads) {
if (!threads?.available) {
return `
Per-Thread Hashrate
${escapeHtml(threads?.reason || 'XMRig REST API not reachable')}
`;
}
const list = threads.threads || [];
const max = Math.max(1, ...list.map((t) => t.hashrate || 0));
const rows = list.map((t) => {
const pct = max > 0 ? Math.round((t.hashrate / max) * 100) : 0;
return `
#${t.index}
${formatHashrate(t.hashrate)}
⏸
▶
`;
}).join('');
return `
Thread Hashrate Share of max Actions
${rows || 'No thread data. '}
`;
}
// ── Pool configuration form ────────────────────────────────────────
function renderPoolConfigForm(poolCfg) {
if (!poolCfg?.available) {
return ``;
}
const current = (poolCfg.pools || [])[0] || {};
return `
`;
}
// ── Thread count form ───────────────────────────────────────────────
function renderThreadsConfigForm(threadsCfg) {
if (!threadsCfg?.available) {
return `Threads config unreachable.
`;
}
return `
`;
}
// ── Algorithm picker ───────────────────────────────────────────────
function renderAlgorithmPicker(algoCfg) {
if (!algoCfg?.available) {
return `Algorithm config unreachable.
`;
}
const presets = algoCfg.presets || [];
const current = algoCfg.current || '';
const options = presets.map((p) =>
`${escapeHtml(p.name)} `
).join('');
return `
${options}
Apply
Force a specific RandomX variant if the auto-detect picks the wrong one. Applies via PUT /1/config.
`;
}
// ── XMRig unreachable card ─────────────────────────────────────────
function renderXmrigUnreachable(svcStatus) {
return `
XMRig Unavailable
The XMRig REST API at http://127.0.0.1:18088 is not reachable.
Start xmrig.service below, or install XMRig if absent.
Arch: pacman -S xmrig · Debian: apt install xmrig · Fedora: build from source (no official package).
XMRig needs --http-host 127.0.0.1 --http-port 18088 in its systemd unit
for the REST API to be reachable.
${renderServiceControls(svcStatus)}
`;
}
// ── Event wiring ────────────────────────────────────────────────────
function wireEvents(panel, { bridge, EventBus }) {
const outputCard = panel.querySelector('#mining-output-card');
const outputPre = panel.querySelector('#mining-output-pre');
const outputTitle = panel.querySelector('#mining-output-title');
const showOutput = (title, text, isError = false) => {
if (!outputCard || !outputPre) return;
outputCard.style.display = 'block';
outputTitle.textContent = title;
outputPre.textContent = text;
outputPre.style.color = isError ? 'var(--sysdeck-accent-danger)' : 'var(--sysdeck-fg)';
};
panel.querySelector('#btn-mining-output-close')?.addEventListener('click', () => {
if (outputCard) outputCard.style.display = 'none';
});
// Service controls.
const serviceOp = async (op, label, okKey) => {
showOutput(`${label} xmrig.service`, `${label} ... (cockpit will prompt for auth)`);
try {
const r = await bridge.mining[op]();
const ok = r[okKey] ?? (r.rc === 0);
showOutput(`${label} xmrig.service — ${ok ? 'success' : 'failed'}`,
`rc=${r.rc}\noutput: ${r.output || '(empty)'}\nstderr: ${r.stderr || '(empty)'}`,
!ok);
if (ok) setTimeout(() => mount(panel, { bridge, EventBus }), 1000);
} catch (err) {
showOutput(`${label} xmrig.service — error`, String(err.message || err), true);
}
};
panel.querySelector('#btn-mining-start')?.addEventListener('click', () => serviceOp('start', 'Start', 'started'));
panel.querySelector('#btn-mining-stop')?.addEventListener('click', () => serviceOp('stop', 'Stop', 'stopped'));
panel.querySelector('#btn-mining-restart')?.addEventListener('click', () => serviceOp('restart', 'Restart', 'restarted'));
panel.querySelector('#btn-mining-refresh')?.addEventListener('click', () => mount(panel, { bridge, EventBus }));
// All-workers controls.
panel.querySelector('#btn-mining-pause-all')?.addEventListener('click', async () => {
showOutput('Pause all', 'Pausing all workers via XMRig JSON-RPC ...');
try {
const r = await bridge.mining.pause();
showOutput(`Pause all — ${r.paused ? 'success' : 'failed'}`, JSON.stringify(r, null, 2), !r.paused);
} catch (err) { showOutput('Pause all — error', String(err.message || err), true); }
});
panel.querySelector('#btn-mining-resume-all')?.addEventListener('click', async () => {
showOutput('Resume all', 'Resuming all workers via XMRig JSON-RPC ...');
try {
const r = await bridge.mining.resume();
showOutput(`Resume all — ${r.resumed ? 'success' : 'failed'}`, JSON.stringify(r, null, 2), !r.resumed);
} catch (err) { showOutput('Resume all — error', String(err.message || err), true); }
});
// Per-worker pause/resume.
panel.querySelectorAll('.btn-pause-worker').forEach((btn) => {
btn.addEventListener('click', async () => {
const id = parseInt(btn.dataset.id, 10);
showOutput(`Pause worker ${id}`, `Pausing worker ${id} ...`);
try {
const r = await bridge.mining.pauseWorker(id);
showOutput(`Pause worker ${id} — ${r.paused ? 'success' : 'failed'}`, JSON.stringify(r, null, 2), !r.paused);
} catch (err) { showOutput(`Pause worker ${id} — error`, String(err.message || err), true); }
});
});
panel.querySelectorAll('.btn-resume-worker').forEach((btn) => {
btn.addEventListener('click', async () => {
const id = parseInt(btn.dataset.id, 10);
showOutput(`Resume worker ${id}`, `Resuming worker ${id} ...`);
try {
const r = await bridge.mining.resumeWorker(id);
showOutput(`Resume worker ${id} — ${r.resumed ? 'success' : 'failed'}`, JSON.stringify(r, null, 2), !r.resumed);
} catch (err) { showOutput(`Resume worker ${id} — error`, String(err.message || err), true); }
});
});
// Pool config.
panel.querySelector('#btn-mining-pool-set')?.addEventListener('click', async () => {
const url = panel.querySelector('#mining-pool-url')?.value?.trim();
const user = panel.querySelector('#mining-pool-user')?.value?.trim();
const pass = panel.querySelector('#mining-pool-pass')?.value?.trim() || 'x';
if (!url || !user) {
showOutput('Set pool', 'Pool URL and wallet address are both required.', true);
return;
}
showOutput('Set pool', `PUT /1/config — pool.url=${url} pool.user=${user} ...`);
try {
const r = await bridge.mining.poolConfigSet(url, user, pass);
showOutput(`Set pool — ${r.set ? 'success' : 'failed'}`, JSON.stringify(r, null, 2), !r.set);
if (r.set) setTimeout(() => mount(panel, { bridge, EventBus }), 1000);
} catch (err) { showOutput('Set pool — error', String(err.message || err), true); }
});
// Threads config.
panel.querySelector('#btn-mining-threads-set')?.addEventListener('click', async () => {
const count = panel.querySelector('#mining-threads-count')?.value;
if (!count) { showOutput('Set threads', 'Enter a thread count.', true); return; }
showOutput('Set threads', `PUT /1/config — cpu.threads=${count} ...`);
try {
const r = await bridge.mining.threadsConfigSet(count);
showOutput(`Set threads — ${r.set ? 'success' : 'failed'}`, JSON.stringify(r, null, 2), !r.set);
if (r.set) setTimeout(() => mount(panel, { bridge, EventBus }), 1000);
} catch (err) { showOutput('Set threads — error', String(err.message || err), true); }
});
// Algorithm.
panel.querySelector('#btn-mining-algo-set')?.addEventListener('click', async () => {
const id = panel.querySelector('#mining-algo-select')?.value;
if (!id) { showOutput('Set algorithm', 'Select an algorithm preset.', true); return; }
showOutput('Set algorithm', `PUT /1/config — cpu.asm=${id} ...`);
try {
const r = await bridge.mining.algorithmSet(id);
showOutput(`Set algorithm — ${r.set ? 'success' : 'failed'}`, JSON.stringify(r, null, 2), !r.set);
if (r.set) setTimeout(() => mount(panel, { bridge, EventBus }), 1000);
} catch (err) { showOutput('Set algorithm — error', String(err.message || err), true); }
});
}
// ── Utilities ───────────────────────────────────────────────────────
async function safe(p, fallback) {
try {
const v = await p;
return v ?? fallback;
} catch {
return fallback;
}
}
function formatHashrate(h) {
if (!h || h <= 0) return '0 H/s';
if (h >= 1000) return `${(h / 1000).toFixed(2)} kH/s`;
if (h >= 1) return `${h.toFixed(1)} H/s`;
return `${h.toFixed(3)} H/s`;
}
function formatUptime(s) {
if (!s) return '—';
const min = Math.floor(s / 60);
if (min < 60) return `${min} min`;
const hr = Math.floor(min / 60);
if (hr < 24) return `${hr}h ${min % 60}m`;
const d = Math.floor(hr / 24);
return `${d}d ${hr % 24}h`;
}
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function renderSkeleton() {
return ``;
}