Add fleet ops dashboard, Calibrate enforcement, and dead-code cleanup.
Ship live alerts, pool status, AI monitor, remote agent commands, build manager, and uninstall flow; wire Calibrate settings (WS ping, pool traffic log, retention limits) at runtime and exclude server/data from git.
This commit is contained in:
59
server/web/src/components/Visual/SystemStatusBar.tsx
Normal file
59
server/web/src/components/Visual/SystemStatusBar.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api } from '../../api/client';
|
||||
import './VisualComponents.css';
|
||||
|
||||
export default function SystemStatusBar() {
|
||||
const [serverOk, setServerOk] = useState(true);
|
||||
const [agentTotal, setAgentTotal] = useState(0);
|
||||
const [agentOnline, setAgentOnline] = useState(0);
|
||||
const [buildCount, setBuildCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
await api.healthCheck();
|
||||
setServerOk(true);
|
||||
} catch {
|
||||
setServerOk(false);
|
||||
}
|
||||
try {
|
||||
const agents = await api.listAgents();
|
||||
setAgentTotal(agents.length);
|
||||
setAgentOnline(agents.filter((a) => a.status === 'online').length);
|
||||
} catch {
|
||||
setAgentTotal(0);
|
||||
setAgentOnline(0);
|
||||
}
|
||||
try {
|
||||
const builds = await api.listBuilds();
|
||||
setBuildCount(builds.length);
|
||||
} catch {
|
||||
setBuildCount(0);
|
||||
}
|
||||
};
|
||||
poll();
|
||||
const id = setInterval(poll, 15000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="system-status-bar">
|
||||
<span className={`status-pill ${serverOk ? 'ok' : 'bad'}`}>
|
||||
<span className="status-pill-dot" />
|
||||
SERVER {serverOk ? 'UP' : 'DOWN'}
|
||||
</span>
|
||||
<span className={`status-pill ${agentOnline > 0 ? 'ok' : agentTotal > 0 ? 'warn' : ''}`}>
|
||||
<span className="status-pill-dot" />
|
||||
FLEET {agentOnline}/{agentTotal} ONLINE
|
||||
</span>
|
||||
<span className={`status-pill ${buildCount > 0 ? 'ok' : 'warn'}`}>
|
||||
<span className="status-pill-dot" />
|
||||
{buildCount} BUILD{buildCount === 1 ? '' : 'S'}
|
||||
</span>
|
||||
<Link to="/guide" className="status-pill" style={{ marginLeft: 'auto', textDecoration: 'none', color: 'var(--neon-cyan)' }}>
|
||||
📖 GUIDE
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
371
server/web/src/components/Visual/VisualComponents.css
Normal file
371
server/web/src/components/Visual/VisualComponents.css
Normal file
@@ -0,0 +1,371 @@
|
||||
/* Visual pipeline, activity, guide components */
|
||||
|
||||
.pipeline-flow {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.pipeline-step-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.pipeline-step.pipeline-active {
|
||||
border-color: var(--neon-cyan);
|
||||
box-shadow: 0 0 20px rgba(0, 245, 255, 0.15);
|
||||
}
|
||||
|
||||
.pipeline-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(201, 162, 39, 0.15);
|
||||
border: 1px solid rgba(201, 162, 39, 0.35);
|
||||
}
|
||||
|
||||
.pipeline-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.pipeline-text strong {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.pipeline-text span {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-tech);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.pipeline-connector {
|
||||
width: 1.5rem;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--brass), var(--neon-cyan));
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pipeline-compact .pipeline-step {
|
||||
padding: 0.5rem 0.65rem;
|
||||
}
|
||||
|
||||
.fleet-pipeline-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 0.5rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.fleet-pipeline-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fleet-pipeline-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
.fleet-pipeline-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fleet-pipeline-node.ok .fleet-pipeline-dot {
|
||||
background: var(--neon-green);
|
||||
border-color: var(--neon-green);
|
||||
box-shadow: 0 0 12px rgba(74, 222, 128, 0.6);
|
||||
animation: pulse-dot 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.fleet-pipeline-node.pending .fleet-pipeline-dot {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
}
|
||||
|
||||
.fleet-pipeline-label {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.fleet-pipeline-node.ok .fleet-pipeline-label {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
.fleet-pipeline-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
margin: 0 0.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.fleet-pipeline-line.lit {
|
||||
background: linear-gradient(90deg, var(--neon-green), rgba(74, 222, 128, 0.3));
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.15); opacity: 0.85; }
|
||||
}
|
||||
|
||||
.activity-pulse {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.activity-blip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.activity-blip-core {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.activity-blip.ok .activity-blip-core {
|
||||
background: var(--neon-green);
|
||||
box-shadow: 0 0 8px var(--neon-green);
|
||||
}
|
||||
|
||||
.activity-blip.bad .activity-blip-core {
|
||||
background: #f87171;
|
||||
box-shadow: 0 0 8px #f87171;
|
||||
}
|
||||
|
||||
.activity-blip-label {
|
||||
font-size: 0.55rem;
|
||||
font-family: var(--font-tech);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.activity-empty {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.compare-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.compare-card h4 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.compare-card ul {
|
||||
margin: 0 0 1rem;
|
||||
padding-left: 1.1rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.roadmap-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.roadmap-card {
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.roadmap-card h4 {
|
||||
margin: 0.35rem 0 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.roadmap-card p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.roadmap-priority {
|
||||
font-size: 0.6rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.roadmap-high .roadmap-priority {
|
||||
color: #f87171;
|
||||
border: 1px solid rgba(248, 113, 113, 0.4);
|
||||
}
|
||||
|
||||
.roadmap-medium .roadmap-priority {
|
||||
color: var(--neon-amber);
|
||||
border: 1px solid rgba(251, 191, 36, 0.4);
|
||||
}
|
||||
|
||||
.roadmap-low .roadmap-priority {
|
||||
color: var(--neon-cyan);
|
||||
border: 1px solid rgba(0, 245, 255, 0.35);
|
||||
}
|
||||
|
||||
.guide-step-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.guide-step-num {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-family: var(--font-tech);
|
||||
font-size: 1.1rem;
|
||||
border: 1px solid var(--brass);
|
||||
color: var(--neon-amber);
|
||||
background: rgba(201, 162, 39, 0.1);
|
||||
}
|
||||
|
||||
.guide-step-body h4 {
|
||||
margin: 0 0 0.35rem;
|
||||
}
|
||||
|
||||
.guide-step-body p {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.guide-tips {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--neon-cyan);
|
||||
}
|
||||
|
||||
.trouble-card {
|
||||
padding: 0.85rem 1rem;
|
||||
border-left: 3px solid var(--neon-amber);
|
||||
background: rgba(251, 191, 36, 0.06);
|
||||
border-radius: 0 6px 6px 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.trouble-card strong {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--neon-amber);
|
||||
}
|
||||
|
||||
.trouble-card span {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.system-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.5rem 1.5rem;
|
||||
background: rgba(8, 6, 4, 0.85);
|
||||
border-bottom: 1px solid rgba(201, 162, 39, 0.2);
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-tech);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-pill-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.status-pill.ok .status-pill-dot {
|
||||
background: var(--neon-green);
|
||||
box-shadow: 0 0 6px var(--neon-green);
|
||||
}
|
||||
|
||||
.status-pill.warn .status-pill-dot {
|
||||
background: var(--neon-amber);
|
||||
}
|
||||
|
||||
.status-pill.bad .status-pill-dot {
|
||||
background: #f87171;
|
||||
}
|
||||
|
||||
.status-pill.ok {
|
||||
color: var(--neon-green);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.compare-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
137
server/web/src/components/Visual/VisualComponents.tsx
Normal file
137
server/web/src/components/Visual/VisualComponents.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import NeonCard from '../NeonCard/NeonCard';
|
||||
import {
|
||||
FORGE_VS_CALIBRATE,
|
||||
PIPELINE_STEPS,
|
||||
ROADMAP_FEATURES,
|
||||
} from '../../help/cheatSheetContent';
|
||||
import './VisualComponents.css';
|
||||
|
||||
interface PipelineFlowProps {
|
||||
activeStep?: string;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function PipelineFlow({ activeStep, compact }: PipelineFlowProps) {
|
||||
return (
|
||||
<div className={`pipeline-flow ${compact ? 'pipeline-compact' : ''}`}>
|
||||
{PIPELINE_STEPS.map((step, i) => (
|
||||
<div key={step.id} className="pipeline-step-wrap">
|
||||
<div className={`pipeline-step ${activeStep === step.id ? 'pipeline-active' : ''}`}>
|
||||
<div className="pipeline-icon">{step.icon}</div>
|
||||
<div className="pipeline-text">
|
||||
<strong>{step.title}</strong>
|
||||
<span>{step.subtitle}</span>
|
||||
</div>
|
||||
</div>
|
||||
{i < PIPELINE_STEPS.length - 1 && <div className="pipeline-connector" aria-hidden />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FleetPipelineStatusProps {
|
||||
hasBuilds: boolean;
|
||||
agentCount: number;
|
||||
onlineCount: number;
|
||||
hasHashrate: boolean;
|
||||
hasShares: boolean;
|
||||
}
|
||||
|
||||
export function FleetPipelineStatus({
|
||||
hasBuilds,
|
||||
agentCount,
|
||||
onlineCount,
|
||||
hasHashrate,
|
||||
hasShares,
|
||||
}: FleetPipelineStatusProps) {
|
||||
const steps = [
|
||||
{ id: 'forge', label: 'Forged', ok: hasBuilds, hint: 'At least one build exists' },
|
||||
{ id: 'deploy', label: 'Deployed', ok: agentCount > 0, hint: 'Agent registered on dashboard' },
|
||||
{ id: 'connect', label: 'Online', ok: onlineCount > 0, hint: `${onlineCount} node(s) live` },
|
||||
{ id: 'mine', label: 'Hashing', ok: hasHashrate, hint: 'Fleet hashrate > 0' },
|
||||
{ id: 'shares', label: 'Shares', ok: hasShares, hint: 'Shares submitted to pool' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="fleet-pipeline-status">
|
||||
{steps.map((s, i) => (
|
||||
<div key={s.id} className="fleet-pipeline-item">
|
||||
<div className={`fleet-pipeline-node ${s.ok ? 'ok' : 'pending'}`} title={s.hint}>
|
||||
<span className="fleet-pipeline-dot" />
|
||||
<span className="fleet-pipeline-label font-tech">{s.label}</span>
|
||||
</div>
|
||||
{i < steps.length - 1 && <div className={`fleet-pipeline-line ${s.ok ? 'lit' : ''}`} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ActivityPulseProps {
|
||||
items: { id: string; label: string; ok: boolean; time?: string }[];
|
||||
}
|
||||
|
||||
export function ActivityPulse({ items }: ActivityPulseProps) {
|
||||
if (items.length === 0) {
|
||||
return <p className="activity-empty font-tech">Awaiting fleet activity…</p>;
|
||||
}
|
||||
return (
|
||||
<div className="activity-pulse">
|
||||
{items.slice(0, 12).map((item) => (
|
||||
<div key={item.id} className={`activity-blip ${item.ok ? 'ok' : 'bad'}`} title={item.time || item.label}>
|
||||
<span className="activity-blip-core" />
|
||||
<span className="activity-blip-label">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ForgeCalibrateCompareProps {
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function ForgeCalibrateCompare({ compact }: ForgeCalibrateCompareProps) {
|
||||
return (
|
||||
<div className={`compare-grid ${compact ? 'compare-compact' : ''}`}>
|
||||
<NeonCard accent="cyan" className="compare-card">
|
||||
<h4>{FORGE_VS_CALIBRATE.forge.title}</h4>
|
||||
<ul>
|
||||
{FORGE_VS_CALIBRATE.forge.items.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
{!compact && (
|
||||
<Link to="/forge" className="btn btn-outline btn-sm">Go to Forge</Link>
|
||||
)}
|
||||
</NeonCard>
|
||||
<NeonCard accent="amber" className="compare-card">
|
||||
<h4>{FORGE_VS_CALIBRATE.calibrate.title}</h4>
|
||||
<ul>
|
||||
{FORGE_VS_CALIBRATE.calibrate.items.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
{!compact && (
|
||||
<Link to="/settings" className="btn btn-outline btn-sm">Go to Calibrate</Link>
|
||||
)}
|
||||
</NeonCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoadmapGrid() {
|
||||
return (
|
||||
<div className="roadmap-grid">
|
||||
{ROADMAP_FEATURES.map((f) => (
|
||||
<div key={f.title} className={`roadmap-card roadmap-${f.priority}`}>
|
||||
<span className={`roadmap-priority font-tech`}>{f.priority}</span>
|
||||
<h4>{f.title}</h4>
|
||||
<p>{f.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user