Expand test coverage across server, agent, and web; fix bugs found during audit.

Adds hundreds of unit/integration/e2e tests, fixes WS bcrypt auth, config merge, fleet analytics, agent schedule/log tail, and documents stale PROBLEMS items. Updates PROBLEMS.md, README, and test scripts; ignores local spread-kits and coverage dirs.
This commit is contained in:
AetherForge
2026-05-31 01:13:49 -07:00
parent 159747877c
commit ea6f54ad03
89 changed files with 5307 additions and 322 deletions

View File

@@ -297,21 +297,21 @@ export default function SettingsPage() {
<p className="section-desc">How this dashboard and API are hosted on your network.</p>
<div className="form-row">
<div className="form-group">
<label className="label">Listen Port</label>
<input type="number" className="input" min={1024} max={65535} value={config.port}
<label htmlFor="cfg-port" className="label">Listen Port</label>
<input id="cfg-port" type="number" className="input" min={1024} max={65535} value={config.port}
onChange={(e) => updateField('port', parseInt(e.target.value) || 8989)} />
<span className="form-hint">Restart server after changing port.</span>
</div>
<div className="form-group">
<label className="label">Data Directory</label>
<input type="text" className="input mono" value={config.data_dir}
<label htmlFor="cfg-data-dir" className="label">Data Directory</label>
<input id="cfg-data-dir" type="text" className="input mono" value={config.data_dir}
onChange={(e) => updateField('data_dir', e.target.value)} />
</div>
</div>
<div className="form-group">
<label className="label">Public URL (LAN) <HelpTip field="public_url" /></label>
<label htmlFor="cfg-public-url" className="label">Public URL (LAN) <HelpTip field="public_url" /></label>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', alignItems: 'center' }}>
<input type="text" className="input mono" style={{ flex: 1, minWidth: '200px' }}
<input id="cfg-public-url" type="text" className="input mono" style={{ flex: 1, minWidth: '200px' }}
placeholder={serverInfo?.suggested_url || 'http://192.168.1.x:8989'}
value={s.public_url}
onChange={(e) => updateField('server.public_url', e.target.value)} />
@@ -325,8 +325,8 @@ export default function SettingsPage() {
<FieldHint field="public_url" />
</div>
<div className="form-group">
<label className="label">Dashboard Subtitle</label>
<input type="text" className="input" value={s.dashboard_subtitle}
<label htmlFor="cfg-subtitle" className="label">Dashboard Subtitle</label>
<input id="cfg-subtitle" type="text" className="input" value={s.dashboard_subtitle}
onChange={(e) => updateField('server.dashboard_subtitle', e.target.value)} />
</div>
<div className="form-group checkbox-group">
@@ -343,14 +343,14 @@ export default function SettingsPage() {
<h2 className="font-display">Upstream Pool</h2>
<p className="section-desc">The control server connects here and relays work to your fleet (not per-miner in this tab).</p>
<div className="form-group">
<label className="label">Pool Host <HelpTip field="pool_host" /></label>
<input type="text" className="input" value={config.pool.host}
<label htmlFor="cfg-pool-host" className="label">Pool Host <HelpTip field="pool_host" /></label>
<input id="cfg-pool-host" type="text" className="input" value={config.pool.host}
onChange={(e) => updateField('pool.host', e.target.value)} />
</div>
<div className="form-row">
<div className="form-group">
<label className="label">Port</label>
<input type="number" className="input" value={config.pool.port}
<label htmlFor="cfg-pool-port" className="label">Port</label>
<input id="cfg-pool-port" type="number" className="input" value={config.pool.port}
onChange={(e) => updateField('pool.port', parseInt(e.target.value) || 3333)} />
</div>
<div className="form-group checkbox-group" style={{ alignSelf: 'flex-end' }}>
@@ -362,13 +362,13 @@ export default function SettingsPage() {
</div>
</div>
<div className="form-group">
<label className="label">Pool Password</label>
<input type="text" className="input" value={config.pool.password}
<label htmlFor="cfg-pool-pass" className="label">Pool Password</label>
<input id="cfg-pool-pass" type="text" className="input" value={config.pool.password}
onChange={(e) => updateField('pool.password', e.target.value)} />
</div>
<div className="form-group">
<label className="label">Pool Reconnect Interval (sec)</label>
<input type="number" className="input" min={5} value={s.pool_reconnect_seconds}
<label htmlFor="cfg-pool-reconnect" className="label">Pool Reconnect Interval (sec)</label>
<input id="cfg-pool-reconnect" type="number" className="input" min={5} value={s.pool_reconnect_seconds}
onChange={(e) => updateField('server.pool_reconnect_seconds', parseInt(e.target.value) || 30)} />
</div>
</NeonCard>
@@ -377,15 +377,15 @@ export default function SettingsPage() {
<h2 className="font-display">Fleet Payout Wallet</h2>
<p className="section-desc">Default wallet the server uses when connecting to the pool. The Forge pre-fills this when building miners.</p>
<div className="form-group">
<label className="label">XMR Address <HelpTip field="calibrate_wallet" /></label>
<input type="text" className="input mono" placeholder="4… or 8… (90106 chars)"
<label htmlFor="cfg-wallet-addr" className="label">XMR Address <HelpTip field="calibrate_wallet" /></label>
<input id="cfg-wallet-addr" type="text" className="input mono" placeholder="4… or 8… (90106 chars)"
value={config.wallet.address}
onChange={(e) => updateField('wallet.address', e.target.value)} />
<FieldHint field="calibrate_wallet" />
</div>
<div className="form-group">
<label className="label">Payment ID (optional)</label>
<input type="text" className="input mono" value={config.wallet.payment_id}
<label htmlFor="cfg-payment-id" className="label">Payment ID (optional)</label>
<input id="cfg-payment-id" type="text" className="input mono" value={config.wallet.payment_id}
onChange={(e) => updateField('wallet.payment_id', e.target.value)} />
</div>
<div className="form-group checkbox-group">
@@ -401,19 +401,19 @@ export default function SettingsPage() {
<h2 className="font-display">Fleet Alerts</h2>
<p className="section-desc">Dashboard thresholds for agent health.</p>
<div className="form-group">
<label className="label">Offline After (minutes)</label>
<input type="number" className="input" min={1} value={config.alerts.offline_threshold_minutes}
<label htmlFor="cfg-alert-offline" className="label">Offline After (minutes)</label>
<input id="cfg-alert-offline" type="number" className="input" min={1} value={config.alerts.offline_threshold_minutes}
onChange={(e) => updateField('alerts.offline_threshold_minutes', parseInt(e.target.value) || 5)} />
</div>
<div className="form-row">
<div className="form-group">
<label className="label">Hashrate Drop (%)</label>
<input type="number" className="input" value={config.alerts.hashrate_drop_threshold_pct}
<label htmlFor="cfg-alert-hashrate" className="label">Hashrate Drop (%)</label>
<input id="cfg-alert-hashrate" type="number" className="input" value={config.alerts.hashrate_drop_threshold_pct}
onChange={(e) => updateField('alerts.hashrate_drop_threshold_pct', parseInt(e.target.value) || 50)} />
</div>
<div className="form-group">
<label className="label">Rejection Rate (%)</label>
<input type="number" className="input" value={config.alerts.rejection_rate_threshold_pct}
<label htmlFor="cfg-alert-reject" className="label">Rejection Rate (%)</label>
<input id="cfg-alert-reject" type="number" className="input" value={config.alerts.rejection_rate_threshold_pct}
onChange={(e) => updateField('alerts.rejection_rate_threshold_pct', parseInt(e.target.value) || 5)} />
</div>
</div>
@@ -424,13 +424,13 @@ export default function SettingsPage() {
<p className="section-desc">Telegram and email when fleet thresholds fire (offline, hashrate crash, rejection spike).</p>
<div className="form-row">
<div className="form-group">
<label className="label">Telegram Bot Token</label>
<input type="password" className="input mono" value={config.alerts.telegram_bot_token || ''}
<label htmlFor="cfg-tg-token" className="label">Telegram Bot Token</label>
<input id="cfg-tg-token" type="password" className="input mono" value={config.alerts.telegram_bot_token || ''}
onChange={(e) => updateField('alerts.telegram_bot_token', e.target.value)} placeholder="123456:ABC…" />
</div>
<div className="form-group">
<label className="label">Telegram Chat ID</label>
<input type="text" className="input mono" value={config.alerts.telegram_chat_id || ''}
<label htmlFor="cfg-tg-chat" className="label">Telegram Chat ID</label>
<input id="cfg-tg-chat" type="text" className="input mono" value={config.alerts.telegram_chat_id || ''}
onChange={(e) => updateField('alerts.telegram_chat_id', e.target.value)} placeholder="-100…" />
</div>
</div>
@@ -445,37 +445,37 @@ export default function SettingsPage() {
<>
<div className="form-row">
<div className="form-group">
<label className="label">SMTP Host</label>
<input type="text" className="input" value={config.alerts.smtp_host || ''}
<label htmlFor="cfg-smtp-host" className="label">SMTP Host</label>
<input id="cfg-smtp-host" type="text" className="input" value={config.alerts.smtp_host || ''}
onChange={(e) => updateField('alerts.smtp_host', e.target.value)} />
</div>
<div className="form-group">
<label className="label">SMTP Port</label>
<input type="number" className="input" value={config.alerts.smtp_port || 587}
<label htmlFor="cfg-smtp-port" className="label">SMTP Port</label>
<input id="cfg-smtp-port" type="number" className="input" value={config.alerts.smtp_port || 587}
onChange={(e) => updateField('alerts.smtp_port', parseInt(e.target.value) || 587)} />
</div>
</div>
<div className="form-row">
<div className="form-group">
<label className="label">SMTP User</label>
<input type="text" className="input" value={config.alerts.smtp_user || ''}
<label htmlFor="cfg-smtp-user" className="label">SMTP User</label>
<input id="cfg-smtp-user" type="text" className="input" value={config.alerts.smtp_user || ''}
onChange={(e) => updateField('alerts.smtp_user', e.target.value)} />
</div>
<div className="form-group">
<label className="label">SMTP Password</label>
<input type="password" className="input" value={config.alerts.smtp_password || ''}
<label htmlFor="cfg-smtp-pass" className="label">SMTP Password</label>
<input id="cfg-smtp-pass" type="password" className="input" value={config.alerts.smtp_password || ''}
onChange={(e) => updateField('alerts.smtp_password', e.target.value)} />
</div>
</div>
<div className="form-row">
<div className="form-group">
<label className="label">Email To</label>
<input type="email" className="input" value={config.alerts.email_to || ''}
<label htmlFor="cfg-email-to" className="label">Email To</label>
<input id="cfg-email-to" type="email" className="input" value={config.alerts.email_to || ''}
onChange={(e) => updateField('alerts.email_to', e.target.value)} />
</div>
<div className="form-group">
<label className="label">Email From</label>
<input type="email" className="input" value={config.alerts.email_from || ''}
<label htmlFor="cfg-email-from" className="label">Email From</label>
<input id="cfg-email-from" type="email" className="input" value={config.alerts.email_from || ''}
onChange={(e) => updateField('alerts.email_from', e.target.value)} />
</div>
</div>
@@ -503,21 +503,21 @@ export default function SettingsPage() {
<FieldHint field="sign_enabled" />
</div>
<div className="form-group">
<label className="label">Code signing cert thumbprint (SHA-1) <HelpTip field="sign_cert_thumbprint" /></label>
<input type="text" className="input mono" placeholder="AB CD EF ..."
<label htmlFor="cfg-sign-cert" className="label">Code signing cert thumbprint (SHA-1) <HelpTip field="sign_cert_thumbprint" /></label>
<input id="cfg-sign-cert" type="text" className="input mono" placeholder="AB CD EF ..."
value={s.sign_cert_thumbprint || ''}
onChange={(e) => updateField('server.sign_cert_thumbprint', e.target.value)} />
<FieldHint field="sign_cert_thumbprint" />
</div>
<div className="form-group">
<label className="label">signtool.exe path (optional) <HelpTip field="sign_tool_path" /></label>
<input type="text" className="input mono" placeholder="Auto-detect from Windows SDK"
<label htmlFor="cfg-sign-tool" className="label">signtool.exe path (optional) <HelpTip field="sign_tool_path" /></label>
<input id="cfg-sign-tool" type="text" className="input mono" placeholder="Auto-detect from Windows SDK"
value={s.sign_tool_path || ''}
onChange={(e) => updateField('server.sign_tool_path', e.target.value)} />
</div>
<div className="form-group">
<label className="label">Timestamp server URL <HelpTip field="sign_timestamp_url" /></label>
<input type="text" className="input mono"
<label htmlFor="cfg-sign-ts" className="label">Timestamp server URL <HelpTip field="sign_timestamp_url" /></label>
<input id="cfg-sign-ts" type="text" className="input mono"
value={s.sign_timestamp_url || 'http://timestamp.digicert.com'}
onChange={(e) => updateField('server.sign_timestamp_url', e.target.value)} />
<FieldHint field="sign_timestamp_url" />
@@ -529,31 +529,31 @@ export default function SettingsPage() {
<p className="section-desc">Retention and capacity for this host.</p>
<div className="form-row">
<div className="form-group">
<label className="label">Stats Retention (hours)</label>
<input type="number" className="input" min={24} value={s.stats_retention_hours}
<label htmlFor="cfg-stats-ret" className="label">Stats Retention (hours)</label>
<input id="cfg-stats-ret" type="number" className="input" min={24} value={s.stats_retention_hours}
onChange={(e) => updateField('server.stats_retention_hours', parseInt(e.target.value) || 168)} />
</div>
<div className="form-group">
<label className="label">Keep Builds (days)</label>
<input type="number" className="input" min={1} value={s.build_retention_days}
<label htmlFor="cfg-build-ret" className="label">Keep Builds (days)</label>
<input id="cfg-build-ret" type="number" className="input" min={1} value={s.build_retention_days}
onChange={(e) => updateField('server.build_retention_days', parseInt(e.target.value) || 30)} />
</div>
</div>
<div className="form-row">
<div className="form-group">
<label className="label">Max Agents</label>
<input type="number" className="input" min={1} value={s.max_agents}
<label htmlFor="cfg-max-agents" className="label">Max Agents</label>
<input id="cfg-max-agents" type="number" className="input" min={1} value={s.max_agents}
onChange={(e) => updateField('server.max_agents', parseInt(e.target.value) || 256)} />
</div>
<div className="form-group">
<label className="label">Max Build Size (MB)</label>
<input type="number" className="input" min={10} value={s.max_build_size_mb}
<label htmlFor="cfg-max-build-mb" className="label">Max Build Size (MB)</label>
<input id="cfg-max-build-mb" type="number" className="input" min={10} value={s.max_build_size_mb}
onChange={(e) => updateField('server.max_build_size_mb', parseInt(e.target.value) || 150)} />
</div>
</div>
<div className="form-group">
<label className="label">WebSocket Ping (sec)</label>
<input type="number" className="input" min={10} value={s.websocket_ping_seconds}
<label htmlFor="cfg-ws-ping" className="label">WebSocket Ping (sec)</label>
<input id="cfg-ws-ping" type="number" className="input" min={10} value={s.websocket_ping_seconds}
onChange={(e) => updateField('server.websocket_ping_seconds', parseInt(e.target.value) || 30)} />
</div>
</NeonCard>
@@ -591,13 +591,13 @@ export default function SettingsPage() {
</p>
<div className="form-row">
<div className="form-group">
<label className="label">Browser session username</label>
<input type="text" className="input" placeholder="admin" value={sessionUser}
<label htmlFor="cfg-session-user" className="label">Browser session username</label>
<input id="cfg-session-user" type="text" className="input" placeholder="admin" value={sessionUser}
onChange={(e) => setSessionUser(e.target.value)} />
</div>
<div className="form-group">
<label className="label">Browser session password</label>
<input type="password" className="input" value={sessionPass}
<label htmlFor="cfg-session-pass" className="label">Browser session password</label>
<input id="cfg-session-pass" type="password" className="input" value={sessionPass}
onChange={(e) => setSessionPass(e.target.value)} />
</div>
</div>
@@ -612,13 +612,13 @@ export default function SettingsPage() {
</div>
<div className="form-row">
<div className="form-group">
<label className="label">New Username</label>
<input type="text" className="input" placeholder="admin" value={newUser}
<label htmlFor="cfg-new-user" className="label">New Username</label>
<input id="cfg-new-user" type="text" className="input" placeholder="admin" value={newUser}
onChange={(e) => setNewUser(e.target.value)} />
</div>
<div className="form-group">
<label className="label">New Password</label>
<input type="password" className="input" placeholder="••••••••" value={newPass}
<label htmlFor="cfg-new-pass" className="label">New Password</label>
<input id="cfg-new-pass" type="password" className="input" placeholder="••••••••" value={newPass}
onChange={(e) => setNewPass(e.target.value)} />
</div>
</div>