From bf9bd338be98bb138e8f71cec436ffaac268cccb Mon Sep 17 00:00:00 2001
From: Ayush
Date: Tue, 28 Jul 2026 20:50:08 +0530
Subject: [PATCH 1/3] UI: Centralize alerts, field errors, and status dot,
refactored the ui to maintain re-usability
---
src/components/data/ConnectionStatus.tsx | 18 +--
src/components/layout/Shell.tsx | 13 +-
.../mining/AdvancedMiningConfigForm.tsx | 9 +-
src/components/pools/PoolIdentityFields.tsx | 16 +-
src/components/pools/PoolPriorityEditor.tsx | 3 +-
src/components/settings/ConfigurationTab.tsx | 28 ++--
src/components/setup/SetupWizard.tsx | 149 +++++++++---------
.../setup/steps/BitcoinPrereqStep.tsx | 24 +--
src/components/setup/steps/BitcoinSetup.tsx | 62 +++-----
src/components/setup/steps/HashrateStep.tsx | 33 ++--
src/components/setup/steps/JdcConfigStep.tsx | 25 ++-
.../setup/steps/MiningIdentityStep.tsx | 21 ++-
src/components/setup/steps/PoolConfigStep.tsx | 2 +-
src/components/setup/steps/ReviewStart.tsx | 23 +--
.../setup/steps/TranslatorConfigStep.tsx | 20 +--
src/components/ui/alert.tsx | 90 +++++++++++
src/components/ui/field-error.tsx | 25 +++
src/components/ui/status-dot.tsx | 45 ++++++
src/pages/UnifiedDashboard.tsx | 97 ++++++------
19 files changed, 391 insertions(+), 312 deletions(-)
create mode 100644 src/components/ui/alert.tsx
create mode 100644 src/components/ui/field-error.tsx
create mode 100644 src/components/ui/status-dot.tsx
diff --git a/src/components/data/ConnectionStatus.tsx b/src/components/data/ConnectionStatus.tsx
index 164ec85f..77a8ffca 100644
--- a/src/components/data/ConnectionStatus.tsx
+++ b/src/components/data/ConnectionStatus.tsx
@@ -1,4 +1,5 @@
import { cn } from '@/lib/utils';
+import { StatusDot } from '@/components/ui/status-dot';
type ConnectionState = 'connected' | 'connecting' | 'disconnected' | 'error';
@@ -17,23 +18,18 @@ export function ConnectionStatus({
label,
className,
}: ConnectionStatusProps) {
- const stateConfig: Record = {
- connected: { color: 'bg-green-500', text: 'Connected' },
- connecting: { color: 'bg-yellow-500 animate-pulse', text: 'Connecting' },
- disconnected: { color: 'bg-muted-foreground', text: 'Disconnected' },
- error: { color: 'bg-red-500', text: 'Error' },
+ const stateConfig: Record = {
+ connected: { status: 'connected', text: 'Connected' },
+ connecting: { status: 'connecting', text: 'Connecting' },
+ disconnected: { status: 'idle', text: 'Disconnected' },
+ error: { status: 'disconnected', text: 'Error' },
};
const config = stateConfig[state];
return (
-
+
{label || config.text}
diff --git a/src/components/layout/Shell.tsx b/src/components/layout/Shell.tsx
index 08e023d3..0129c394 100644
--- a/src/components/layout/Shell.tsx
+++ b/src/components/layout/Shell.tsx
@@ -7,6 +7,7 @@ import type { AppMode, AppFeatures } from '@/types/api';
import { getAppFeatures } from '@/types/api';
import { useUiConfig } from '@/hooks/useUiConfig';
import { PoolIcon } from '@/components/ui/pool-icon';
+import { StatusDot } from '@/components/ui/status-dot';
function useTheme() {
const [isDark, setIsDark] = useState(() => {
@@ -173,20 +174,12 @@ export function Shell({
<>
{/* Mobile: dot + uptime only (no status text to save space) */}
-
+
Uptime: {formatUptime(uptime ?? 0)}
{/* Desktop: dot + full status text + uptime */}
-
+
{connectionStatus === 'connected' ? (
{connectedStatusLabel}
diff --git a/src/components/mining/AdvancedMiningConfigForm.tsx b/src/components/mining/AdvancedMiningConfigForm.tsx
index 77b2f865..df6d56ca 100644
--- a/src/components/mining/AdvancedMiningConfigForm.tsx
+++ b/src/components/mining/AdvancedMiningConfigForm.tsx
@@ -6,6 +6,7 @@ import {
} from '@sv2-ui/shared';
import { Switch } from '@/components/ui/switch';
+import { FieldError } from '@/components/ui/field-error';
export interface AdvancedMiningConfigValues {
sharesPerMinute: string;
@@ -124,9 +125,7 @@ export function AdvancedMiningConfigForm({
className="h-9 w-full rounded-lg border border-input bg-background px-3 text-sm outline-none transition-all focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15"
/>
{!sharesPerMinuteValid ? (
-
- Enter a value greater than 0.
-
+
) : (
Target share rate for variable difficulty.
@@ -151,9 +150,7 @@ export function AdvancedMiningConfigForm({
className="h-9 w-full rounded-lg border border-input bg-background px-3 text-sm outline-none transition-all focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15"
/>
{!downstreamExtranonce2SizeValid ? (
-
+
) : (
-
+
Use the exact username from your Braiins Pool account. If this value does not match an existing
Braiins account, the pool connection will not be established properly.
-
+
)}
- {error && {error}
}
+
{miningMode === 'solo'
? 'Bitcoin address used by the pool for solo mining payouts.'
@@ -147,9 +147,7 @@ function SriPoolIdentityFields({
autoComplete="off"
className="w-full h-10 px-3 rounded-lg border border-input bg-background focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all font-mono text-sm"
/>
- {getBitcoinAddressError(payoutAddress, network) && (
-
{getBitcoinAddressError(payoutAddress, network)}
- )}
+
Used with worker and donation settings to build this pool identity.
@@ -215,7 +213,7 @@ function SriPoolIdentityFields({
- {identityError && {identityError}
}
+
);
}
diff --git a/src/components/pools/PoolPriorityEditor.tsx b/src/components/pools/PoolPriorityEditor.tsx
index f64275fb..a8effaa9 100644
--- a/src/components/pools/PoolPriorityEditor.tsx
+++ b/src/components/pools/PoolPriorityEditor.tsx
@@ -1,6 +1,7 @@
import { useRef, useState } from 'react';
import { ArrowDown, ArrowUp, GripVertical, X } from 'lucide-react';
import { DEFAULT_POOL_PORT, type MiningMode, type PoolConfig } from '@sv2-ui/shared';
+import { FieldError } from '@/components/ui/field-error';
import { PoolIcon } from '@/components/ui/pool-icon';
import {
createEmptyCustomPool,
@@ -380,7 +381,7 @@ function CustomPoolFields({
pubkeyError ? 'border-destructive focus-visible:border-destructive' : 'border-input focus-visible:border-primary'
}`}
/>
- {pubkeyError && {pubkeyError}
}
+
diff --git a/src/components/settings/ConfigurationTab.tsx b/src/components/settings/ConfigurationTab.tsx
index d7f77073..e8cd61c4 100644
--- a/src/components/settings/ConfigurationTab.tsx
+++ b/src/components/settings/ConfigurationTab.tsx
@@ -55,7 +55,8 @@ import {
Check,
X,
} from 'lucide-react';
-
+import { FieldError } from '@/components/ui/field-error';
+import { StatusDot } from '@/components/ui/status-dot';
const SETUP_TARGET_STEP_STORAGE_KEY = 'sv2-ui-setup-target-step';
type EditingField = null | 'pools' | 'mode' | 'signature' | 'hashrate' | 'telemetry' | 'advanced';
@@ -356,10 +357,10 @@ export function ConfigurationTab() {
const updateEditPoolIdentity = (index: number, nextPool: PoolConfig) => {
setEditPools((currentPools) => currentPools
? normalizePoolPriorityIdentities(
- currentPools.map((pool, poolIndex) => poolIndex === index ? nextPool : pool),
- currentPools[0],
- activeMiningMode,
- )
+ currentPools.map((pool, poolIndex) => poolIndex === index ? nextPool : pool),
+ currentPools[0],
+ activeMiningMode,
+ )
: null);
};
@@ -370,7 +371,7 @@ export function ConfigurationTab() {
-
+
{isRunning ? 'Services Running' : 'Services Stopped'}
@@ -525,11 +526,10 @@ export function ConfigurationTab() {
key={m}
type="button"
onClick={() => setEditMode(m)}
- className={`px-4 py-2 rounded-lg border text-sm font-medium transition-all ${
- editMode === m
+ className={`px-4 py-2 rounded-lg border text-sm font-medium transition-all ${editMode === m
? 'border-primary bg-primary/[0.04] text-primary'
: 'border-border bg-card hover:border-primary/45'
- }`}
+ }`}
>
{m === 'jd' ? 'Job Declaration (Custom Templates)' : 'Pool Templates'}
@@ -687,8 +687,8 @@ export function ConfigurationTab() {
placeholder="Miner signature"
className="w-full h-10 px-3 rounded-lg border border-input bg-background font-mono text-sm focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all"
/>
- {editSignature && getIdentifierError(editSignature) && (
-
{getIdentifierError(editSignature)}
+ {editSignature && (
+
)}
Miner-chosen tag shown in coinbase transactions on block explorers.
@@ -766,9 +766,7 @@ export function ConfigurationTab() {
autoComplete="off"
className="w-full h-9 px-3 rounded-lg border border-input bg-background font-mono text-sm focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all"
/>
- {minerTelemetryCidrError && (
-
{minerTelemetryCidrError}
- )}
+
Private LAN subnet where miners expose their web/API interface.
@@ -977,4 +975,4 @@ function PoolSummary({
);
-}
+}
\ No newline at end of file
diff --git a/src/components/setup/SetupWizard.tsx b/src/components/setup/SetupWizard.tsx
index bb1ac6d0..dd31186f 100644
--- a/src/components/setup/SetupWizard.tsx
+++ b/src/components/setup/SetupWizard.tsx
@@ -1,9 +1,10 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { useLocation } from 'wouter';
-import { ArrowLeft, AlertCircle, Sun, Moon } from 'lucide-react';
+import { ArrowLeft, Sun, Moon } from 'lucide-react';
import { SetupStep, SetupData, initialSetupData } from './types';
import { shouldAggregateTranslatorChannelsForPools } from './poolRules';
import { BITCOIN_MESSAGES } from '@/lib/messages';
+import { Alert } from '@/components/ui/alert';
function useTheme() {
const [isDark, setIsDark] = useState(() => {
@@ -89,7 +90,6 @@ export function SetupWizard() {
const [currentStep, setCurrentStep] = useState
('mining-mode');
const [data, setData] = useState(initialSetupData);
const [isReconfiguring, setIsReconfiguring] = useState(false);
- const [isSetupReview, setIsSetupReview] = useState(false);
const [loadingConfig, setLoadingConfig] = useState(true);
const [bitcoinSetupNotice, setBitcoinSetupNotice] = useState(null);
const { results: discoveredNodes, isLoading: isDiscovering, retry: retryDiscovery } = useBitcoinRpcDiscovery();
@@ -110,7 +110,6 @@ export function SetupWizard() {
const setupReviewRequested = window.sessionStorage.getItem(SETUP_REVIEW_STORAGE_KEY) === 'true';
window.sessionStorage.removeItem(SETUP_TARGET_STEP_STORAGE_KEY);
window.sessionStorage.removeItem(SETUP_REVIEW_STORAGE_KEY);
- setIsSetupReview(setupReviewRequested);
if (targetStep === 'bitcoin' && config.bitcoin) {
nextConfig = {
@@ -252,85 +251,81 @@ export function SetupWizard() {
return (
<>
-
- {/* Header */}
-
-
-
- Back
-
-
-
- {nonModeSteps.map((_, idx) => (
-
- ))}
-
+
+ {/* Header */}
+
+
+
+ Back
+
+
+
+ {nonModeSteps.map((_, idx) => (
+
+ ))}
+
-
-
+
+
- {/* Step content */}
-
-
-
- {isReconfiguring && currentStepIndex === 1 && (
-
-
- {isSetupReview
- ? 'Review your setup to continue mining. Your saved settings are prefilled.'
- : 'Reconfiguring SV2 setup — this will replace your current configuration.'}
-
- )}
- {currentStep === 'template-mode' &&
}
- {currentStep === 'pool' &&
}
- {currentStep === 'bitcoin-prereq' && (
-
- )}
- {currentStep === 'bitcoin' && (
-
setBitcoinSetupNotice(null)}
- discoveredNodes={discoveredNodes}
- />
- )}
- {currentStep === 'hashrate' && }
- {currentStep === 'identity' && }
- {currentStep === 'review' && (
-
- )}
+ {/* Step content */}
+
+
+
+ {isReconfiguring && currentStepIndex === 1 && (
+
+ Reconfiguring SV2 setup — this will replace your current configuration.
+
+ )}
+ {currentStep === 'template-mode' &&
}
+ {currentStep === 'pool' &&
}
+ {currentStep === 'bitcoin-prereq' && (
+
+ )}
+ {currentStep === 'bitcoin' && (
+
setBitcoinSetupNotice(null)}
+ discoveredNodes={discoveredNodes}
+ />
+ )}
+ {currentStep === 'hashrate' && }
+ {currentStep === 'identity' && }
+ {currentStep === 'review' && (
+
+ )}
+
-
- {flashOverlay}
+ {flashOverlay}
>
);
}
diff --git a/src/components/setup/steps/BitcoinPrereqStep.tsx b/src/components/setup/steps/BitcoinPrereqStep.tsx
index 5c605ef3..ae497141 100644
--- a/src/components/setup/steps/BitcoinPrereqStep.tsx
+++ b/src/components/setup/steps/BitcoinPrereqStep.tsx
@@ -12,7 +12,8 @@ import {
} from '@sv2-ui/shared';
import { BITCOIN_MESSAGES } from '@/lib/messages';
import { StepProps, BitcoinConfig } from '../types';
-import { Copy, Check, ExternalLink, Loader2, RotateCw, CheckCircle2, AlertCircle } from 'lucide-react';
+import { Check, Loader2, AlertCircle, CheckCircle2, RotateCw, Copy, ExternalLink } from 'lucide-react';
+import { Alert } from '@/components/ui/alert';
import type { BitcoinRpcDiscoveryResult } from '@/hooks/useBitcoinRpcDiscovery';
import { useHostEnv } from '@/hooks/useHostEnv';
import { BitcoinNetworkSelector } from '../BitcoinNetworkSelector';
@@ -285,12 +286,6 @@ export function BitcoinPrereqStep({ data, updateData, onNext, discoveredNodes, i
description: `${detectedNodeSummary}. Continue to configure the connection.`,
};
- const readinessClasses = {
- neutral: 'border-border bg-muted/50 text-muted-foreground',
- warning: 'border-warning/20 bg-warning/[0.08] text-warning',
- destructive: 'border-destructive/20 bg-destructive/[0.08] text-destructive',
- success: 'border-success/20 bg-success/10 text-success',
- }[readiness.tone];
const canConfigureManually = !hostOsLoading
&& !isDiscovering
@@ -380,17 +375,14 @@ export function BitcoinPrereqStep({ data, updateData, onNext, discoveredNodes, i
/>
-
-
{readiness.icon}
-
-
{readiness.title}
-
{readiness.description}
-
-
+
{readiness.title}
+
{readiness.description}
+
{(canConfigureManually || canRetry) && (
diff --git a/src/components/setup/steps/BitcoinSetup.tsx b/src/components/setup/steps/BitcoinSetup.tsx
index 98c78ae5..9ea20c7f 100644
--- a/src/components/setup/steps/BitcoinSetup.tsx
+++ b/src/components/setup/steps/BitcoinSetup.tsx
@@ -12,7 +12,9 @@ import {
import type { BitcoinCoreVersion, OperatingSystem, BitcoinNetwork } from '@sv2-ui/shared';
import { BITCOIN_MESSAGES } from '@/lib/messages';
import { StepProps, BitcoinConfig } from '../types';
-import { Apple, Terminal, Pencil, Check, Loader2, AlertCircle, CheckCircle2, RotateCw } from 'lucide-react';
+import { Apple, Terminal, Pencil, Check, Loader2, RotateCw } from 'lucide-react';
+import { Alert } from '@/components/ui/alert';
+import { FieldError } from '@/components/ui/field-error';
import { UmbrelIcon } from '../icons/UmbrelIcon';
import { useBitcoinSocketValidation } from '@/hooks/useBitcoinSocketValidation';
import type { BitcoinRpcDiscoveryResult } from '@/hooks/useBitcoinRpcDiscovery';
@@ -106,19 +108,14 @@ export function BitcoinSetup({ data, updateData, onNext, notice, onDismissNotice
{discoveryApplied && (
-
-
+
Pre-filled from detected node
Network: {network} • Version: {detectedVersionLabel}
-
+
)}
@@ -184,14 +181,9 @@ export function BitcoinSetup({ data, updateData, onNext, notice, onDismissNotice
{BITCOIN_MESSAGES.versionLabel}
{notice && (
-
+
)}
{!coreVersion && (
-
- {BITCOIN_MESSAGES.selectVersionPrompt}
-
+
)}
{os === 'umbrel' && coreVersion && !isChecking && socketError && (
-
-
+
Umbrel Setup Instructions
@@ -229,7 +218,7 @@ export function BitcoinSetup({ data, updateData, onNext, notice, onDismissNotice
Enable IPC Mining Interface
-
+
)}
Click to edit if your socket is in a different location.
{isChecking && (
-
-
+
}>
Checking socket path...
-
+
)}
{!isChecking && isValid && (
-
-
+
Socket is listening
-
+
)}
{!isChecking && socketError && (
-
-
+
{socketError}
retrySocketValidation()}
disabled={isRefreshing}
- className="inline-flex h-8 items-center gap-2 rounded-md border border-destructive/30 bg-background px-3 text-xs font-medium text-destructive transition-colors hover:bg-destructive/[0.06] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/30 disabled:cursor-not-allowed disabled:opacity-60"
+ className="inline-flex h-8 items-center gap-2 rounded-md border border-red-500/30 bg-background px-3 text-xs font-medium text-red-600 dark:text-red-500 transition-colors hover:bg-red-500/[0.06] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/30 disabled:cursor-not-allowed disabled:opacity-60"
>
{isRefreshing ? (
@@ -331,10 +305,10 @@ export function BitcoinSetup({ data, updateData, onNext, notice, onDismissNotice
{isRefreshing ? 'Checking...' : 'Retry'}
{isRetryable && (
-
Rechecking automatically while Bitcoin Core starts.
+
Rechecking automatically while Bitcoin Core starts.
)}
-
+
)}
diff --git a/src/components/setup/steps/HashrateStep.tsx b/src/components/setup/steps/HashrateStep.tsx
index 9a1c2c60..1d902b43 100644
--- a/src/components/setup/steps/HashrateStep.tsx
+++ b/src/components/setup/steps/HashrateStep.tsx
@@ -6,6 +6,8 @@ import {
normalizeMinerTelemetryCidr,
} from '@sv2-ui/shared';
import { Check, ChevronDown, Settings2 } from 'lucide-react';
+import { Alert } from '@/components/ui/alert';
+import { FieldError } from '@/components/ui/field-error';
import {
AdvancedMiningConfigForm,
createAdvancedMiningConfigValues,
@@ -23,10 +25,10 @@ interface HashratePreset {
}
const HASHRATE_PRESETS: HashratePreset[] = [
- { id: 'bitaxe', label: 'Bitaxe / USB Miner', hashrate: 500_000_000_000, description: '~500 GH/s' },
- { id: 'mid-asic', label: 'Mid-Range ASIC', hashrate: DEFAULT_MIN_HASHRATE, description: '~100 TH/s' },
- { id: 'high-asic', label: 'High-End ASIC', hashrate: 300_000_000_000_000, description: '~300 TH/s' },
- { id: 'custom', label: 'Custom', hashrate: 0, description: 'Enter your own value' },
+ { id: 'bitaxe', label: 'Bitaxe / USB Miner', hashrate: 500_000_000_000, description: '~500 GH/s' },
+ { id: 'mid-asic', label: 'Mid-Range ASIC', hashrate: DEFAULT_MIN_HASHRATE, description: '~100 TH/s' },
+ { id: 'high-asic', label: 'High-End ASIC', hashrate: 300_000_000_000_000, description: '~300 TH/s' },
+ { id: 'custom', label: 'Custom', hashrate: 0, description: 'Enter your own value' },
];
export function HashrateStep({ data, updateData, onNext }: StepProps) {
@@ -76,9 +78,9 @@ export function HashrateStep({ data, updateData, onNext }: StepProps) {
downstream_extranonce2_size: parsedAdvancedConfig.downstreamExtranonce2Size,
},
});
- // intentionally excluded: data.translator and updateData cause infinite loop when included
+ // intentionally excluded: data.translator and updateData cause infinite loop when included
// eslint-disable-next-line react-hooks/exhaustive-deps
-}, [hashrate, minerTelemetryCidr, advancedConfig, isSoloPool]);
+ }, [hashrate, minerTelemetryCidr, advancedConfig, isSoloPool]);
return (
@@ -89,14 +91,14 @@ export function HashrateStep({ data, updateData, onNext }: StepProps) {
-
-
+
+
Difficulty per worker is automatically adjusted via variable difficulty (vardiff) algorithm.
Give it a starting point. Using the approximate hashrate of your{' '}
lowest performing worker ensures every
device can find shares right away.
-
+
Select hashrate preset
@@ -108,9 +110,8 @@ export function HashrateStep({ data, updateData, onNext }: StepProps) {
type="button"
onClick={() => handlePresetChange(preset.id)}
aria-pressed={active}
- className={`relative p-4 rounded-xl border transition-all text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${
- active ? 'border-primary bg-primary/[0.04]' : 'border-border bg-card hover:border-primary/45 hover:bg-primary/[0.02]'
- }`}
+ className={`relative p-4 rounded-xl border transition-all text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${active ? 'border-primary bg-primary/[0.04]' : 'border-border bg-card hover:border-primary/45 hover:bg-primary/[0.02]'
+ }`}
>
{active &&
}
@@ -157,11 +158,7 @@ export function HashrateStep({ data, updateData, onNext }: StepProps) {
aria-describedby="miner-telemetry-cidr-desc miner-telemetry-cidr-error"
className="w-full h-10 px-3 rounded-lg border border-input bg-background font-mono text-sm focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all"
/>
- {minerTelemetryCidrError && (
-
- {minerTelemetryCidrError}
-
- )}
+
Recommended for better telemetry. Use the private LAN subnet where miners expose
their web/API interface.
@@ -210,4 +207,4 @@ export function HashrateStep({ data, updateData, onNext }: StepProps) {
);
-}
+}
\ No newline at end of file
diff --git a/src/components/setup/steps/JdcConfigStep.tsx b/src/components/setup/steps/JdcConfigStep.tsx
index ff1e1bbd..1053c634 100644
--- a/src/components/setup/steps/JdcConfigStep.tsx
+++ b/src/components/setup/steps/JdcConfigStep.tsx
@@ -1,7 +1,9 @@
import { useState, useEffect } from 'react';
import { StepProps, JdcConfig } from '../types';
-import { Info } from 'lucide-react';
+
import { isValidBitcoinAddress, getBitcoinAddressError, getBitcoinAddressPlaceholder } from '@/lib/utils';
+import { Alert } from '@/components/ui/alert';
+import { FieldError } from '@/components/ui/field-error';
export function JdcConfigStep({ data, updateData, onNext }: StepProps) {
const [config, setConfig] = useState
(
@@ -35,17 +37,12 @@ export function JdcConfigStep({ data, updateData, onNext }: StepProps) {
-
-
-
-
-
- The JD Client connects to the pool and declares your custom block templates.
- The coinbase reward address is used as a fallback for solo mining if the pool connection fails.
-
-
-
-
+
+
+ The JD Client connects to the pool and declares your custom block templates.
+ The coinbase reward address is used as a fallback for solo mining if the pool connection fails.
+
+
@@ -63,9 +60,7 @@ export function JdcConfigStep({ data, updateData, onNext }: StepProps) {
autoComplete="off"
className="w-full h-10 px-3 rounded-lg border border-input bg-background font-mono text-sm focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all"
/>
- {getBitcoinAddressError(config.coinbase_reward_address, network) && (
-
{getBitcoinAddressError(config.coinbase_reward_address, network)}
- )}
+
Bitcoin address for receiving mining rewards (fallback for solo mining)
diff --git a/src/components/setup/steps/MiningIdentityStep.tsx b/src/components/setup/steps/MiningIdentityStep.tsx
index b6f41f73..2c3ec230 100644
--- a/src/components/setup/steps/MiningIdentityStep.tsx
+++ b/src/components/setup/steps/MiningIdentityStep.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { StepProps } from '../types';
-import { Info } from 'lucide-react';
+
import {
getBitcoinAddressError,
getBitcoinAddressPlaceholder,
@@ -8,6 +8,8 @@ import {
isTomlSafeIdentifier,
isValidBitcoinAddress,
} from '@/lib/utils';
+import { Alert } from '@/components/ui/alert';
+import { FieldError } from '@/components/ui/field-error';
export function MiningIdentityStep({ data, updateData, onNext }: StepProps) {
const isSoloMode = data.miningMode === 'solo';
@@ -58,8 +60,8 @@ export function MiningIdentityStep({ data, updateData, onNext }: StepProps) {
autoComplete="off"
className="w-full h-10 px-3 rounded-lg border border-input bg-background focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-primary/15 outline-none transition-all font-mono text-sm"
/>
- {minerSignature && getIdentifierError(minerSignature) && (
-
{getIdentifierError(minerSignature)}
+ {minerSignature && (
+
)}
Miner-chosen tag shown in coinbase transactions on block explorers
@@ -72,12 +74,9 @@ export function MiningIdentityStep({ data, updateData, onNext }: StepProps) {
(required)
-
-
-
- {coinbaseNotice}
-
-
+
+ {coinbaseNotice}
+
- {getBitcoinAddressError(coinbaseAddress, network) && (
-
{getBitcoinAddressError(coinbaseAddress, network)}
- )}
+
Bitcoin address that receives solo mining rewards
diff --git a/src/components/setup/steps/PoolConfigStep.tsx b/src/components/setup/steps/PoolConfigStep.tsx
index c7ee8868..1b8ee284 100644
--- a/src/components/setup/steps/PoolConfigStep.tsx
+++ b/src/components/setup/steps/PoolConfigStep.tsx
@@ -214,4 +214,4 @@ function SelectedPoolSummary({
);
-}
+}
\ No newline at end of file
diff --git a/src/components/setup/steps/ReviewStart.tsx b/src/components/setup/steps/ReviewStart.tsx
index 487d3e77..9ccc20ab 100644
--- a/src/components/setup/steps/ReviewStart.tsx
+++ b/src/components/setup/steps/ReviewStart.tsx
@@ -1,7 +1,8 @@
import React, { useState, useEffect } from "react";
import { SetupStep, StepProps } from "../types";
-import { Loader2, AlertCircle } from "lucide-react";
+import { Loader2 } from "lucide-react";
import { useQueryClient } from "@tanstack/react-query";
+import { Alert } from "@/components/ui/alert";
import { MinerConnectionInfo } from "../MinerConnectionInfo";
import { shouldAggregateTranslatorChannelsForPools } from "../poolRules";
import { isBitcoinSocketError } from "@/lib/bitcoinSocketErrors";
@@ -189,17 +190,9 @@ export function ReviewStart({ data, onComplete, onGoToStep }: ReviewStartProps)
{error && (
-
-
+
-
+
Error
{error}
@@ -210,13 +203,13 @@ export function ReviewStart({ data, onComplete, onGoToStep }: ReviewStartProps)
setError(null);
onGoToStep("bitcoin");
}}
- className="mt-3 inline-flex h-9 items-center justify-center rounded-full bg-destructive px-4 text-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/30"
+ className="mt-3 inline-flex h-9 items-center justify-center rounded-full bg-red-600 px-4 text-sm font-medium text-white transition-colors hover:bg-red-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/30"
>
Open Bitcoin Setup
)}
-
+
)}
{/* Summary */}
@@ -264,12 +257,12 @@ export function ReviewStart({ data, onComplete, onGoToStep }: ReviewStartProps)
{isAggregatedTproxy && (
-
+
Translator aggregation is enabled for Braiins compatibility.
The Translator Proxy will aggregate all SV1 workers into one
single SV2 upstream channel, so the Braiins Pool dashboard
will not track workers individually.
-
+
)}
diff --git a/src/components/setup/steps/TranslatorConfigStep.tsx b/src/components/setup/steps/TranslatorConfigStep.tsx
index dfee1bd1..8191d7da 100644
--- a/src/components/setup/steps/TranslatorConfigStep.tsx
+++ b/src/components/setup/steps/TranslatorConfigStep.tsx
@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react';
import { StepProps, TranslatorConfig } from '../types';
import { Switch } from '@/components/ui/switch';
-import { Info } from 'lucide-react';
+
import { TRANSLATOR_PORT } from '@/lib/ports';
+import { Alert } from '@/components/ui/alert';
export function TranslatorConfigStep({ data, updateData, onNext }: StepProps) {
const isSoloMode = data.miningMode === 'solo';
@@ -36,17 +37,12 @@ export function TranslatorConfigStep({ data, updateData, onNext }: StepProps) {
-
-
-
-
-
- The Translator Proxy bridges your SV1 mining hardware to the SV2 {isSoloMode ? 'solo pool' : 'pool'}.
- Your miners will connect to the Translator on port {TRANSLATOR_PORT}.
-
-
-
-
+
+
+ The Translator Proxy bridges your SV1 mining hardware to the SV2 {isSoloMode ? 'solo pool' : 'pool'}.
+ Your miners will connect to the Translator on port {TRANSLATOR_PORT}.
+
+
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 00000000..3bae18d7
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,90 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+import { AlertCircle, CheckCircle2, Info } from 'lucide-react';
+
+const alertVariants = cva(
+ 'relative w-full rounded-xl border p-4 text-sm flex gap-3 text-left',
+ {
+ variants: {
+ variant: {
+ neutral: 'bg-muted/50 border-border text-muted-foreground',
+ warning: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-600 dark:text-yellow-500',
+ destructive: 'bg-red-500/10 border-red-500/20 text-red-600 dark:text-red-500',
+ success: 'bg-green-500/10 border-green-500/20 text-green-600 dark:text-green-500',
+ info: 'bg-blue-500/10 border-blue-500/20 text-blue-600 dark:text-blue-500',
+ },
+ },
+ defaultVariants: {
+ variant: 'neutral',
+ },
+ }
+);
+
+export interface AlertProps
+ extends React.HTMLAttributes
,
+ VariantProps {
+ icon?: React.ReactNode;
+}
+
+const Alert = React.forwardRef(
+ ({ className, variant, icon, children, role, ...props }, ref) => {
+ // Auto-assign ARIA roles based on variant if not explicitly provided
+ const defaultRole = variant === 'destructive' || variant === 'warning' ? 'alert' : 'status';
+
+ // Auto-assign default icons if not explicitly overridden (can pass icon={null} to remove)
+ let DefaultIcon = null;
+ if (icon === undefined) {
+ if (variant === 'destructive' || variant === 'warning') DefaultIcon = AlertCircle;
+ else if (variant === 'success') DefaultIcon = CheckCircle2;
+ else if (variant === 'info') DefaultIcon = Info;
+ }
+
+ return (
+
+ {(icon !== undefined ? icon : DefaultIcon) && (
+
+ {icon !== undefined ? icon : DefaultIcon && }
+
+ )}
+
+ {children}
+
+
+ );
+ }
+);
+Alert.displayName = 'Alert';
+
+// Reusable alert title component
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = 'AlertTitle';
+
+// Reusable alert description component
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = 'AlertDescription';
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/src/components/ui/field-error.tsx b/src/components/ui/field-error.tsx
new file mode 100644
index 00000000..ec4c1892
--- /dev/null
+++ b/src/components/ui/field-error.tsx
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { cn } from '@/lib/utils';
+
+export interface FieldErrorProps extends React.HTMLAttributes {
+ message?: string | null;
+}
+
+const FieldError = React.forwardRef(
+ ({ className, message, ...props }, ref) => {
+ if (!message) return null;
+
+ return (
+
+ {message}
+
+ );
+ }
+);
+FieldError.displayName = 'FieldError';
+
+export { FieldError };
diff --git a/src/components/ui/status-dot.tsx b/src/components/ui/status-dot.tsx
new file mode 100644
index 00000000..e266f91d
--- /dev/null
+++ b/src/components/ui/status-dot.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+import { cn } from '@/lib/utils';
+
+const statusDotVariants = cva(
+ 'shrink-0 rounded-full',
+ {
+ variants: {
+ status: {
+ connected: 'bg-green-500',
+ connecting: 'bg-yellow-500 animate-pulse',
+ disconnected: 'bg-red-500',
+ idle: 'bg-muted-foreground',
+ },
+ size: {
+ default: 'h-2.5 w-2.5',
+ sm: 'h-2 w-2',
+ lg: 'h-3 w-3',
+ },
+ },
+ defaultVariants: {
+ status: 'idle',
+ size: 'default',
+ },
+ }
+);
+
+export interface StatusDotProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+const StatusDot = React.forwardRef(
+ ({ className, status, size, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+StatusDot.displayName = 'StatusDot';
+
+export { StatusDot };
diff --git a/src/pages/UnifiedDashboard.tsx b/src/pages/UnifiedDashboard.tsx
index ec2c07ba..6bc8f9a6 100644
--- a/src/pages/UnifiedDashboard.tsx
+++ b/src/pages/UnifiedDashboard.tsx
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Link } from 'wouter';
import { useQueryClient } from '@tanstack/react-query';
import { AlertTriangle, Search, Play, Trash2 } from 'lucide-react';
+import { Alert, AlertTitle } from '@/components/ui/alert';
import { InfoPopover } from '@/components/ui/info-popover';
import { MinerConnectionInfo } from '@/components/setup/MinerConnectionInfo';
import { Shell } from '@/components/layout/Shell';
@@ -117,10 +118,10 @@ export function UnifiedDashboard() {
const { data: translatorOk, isLoading: translatorHealthLoading, isError: translatorHealthError } = useTranslatorHealth();
const { data: jdcOk, isLoading: jdcHealthLoading, isError: jdcHealthError } = useJdcHealth(isJdMode);
const translatorHealthy = translatorOk === true && !translatorHealthError;
- const jdcHealthy = jdcOk === true && !jdcHealthError;
- const translatorDown = !translatorHealthLoading && !translatorHealthy;
- const jdcDown = isJdMode && !jdcHealthLoading && !jdcHealthy;
- const showError = poolError || translatorDown || jdcDown;
+ const jdcHealthy = jdcOk === true && !jdcHealthError;
+ const translatorDown = !translatorHealthLoading && !translatorHealthy;
+ const jdcDown = isJdMode && !jdcHealthLoading && !jdcHealthy;
+ const showError = poolError || translatorDown || jdcDown;
const configuredButStopped = isOrchestrated && isConfigured && !isRunning;
const configurationIssue = configurationIssues[0] ?? null;
const canReviewConfiguration = configurationIssue?.code !== 'saved-setup-unavailable';
@@ -293,10 +294,10 @@ export function UnifiedDashboard() {
// Total hashrate:
// - JD mode: from SV2 client telemetry when available, falling back to vardiff totals
// - Translator-only mode: from SV1 client telemetry when available, falling back to vardiff totals
- const totalHashrate = isJdMode
+ const totalHashrate = isJdMode
? (sv2Clients && sv1Data
- ? (sv2TotalHashrate ?? 0) + sv1TotalHashrate
- : (poolGlobal?.sv2_clients?.total_hashrate ?? 0))
+ ? (sv2TotalHashrate ?? 0) + sv1TotalHashrate
+ : (poolGlobal?.sv2_clients?.total_hashrate ?? 0))
: (sv1Data ? sv1TotalHashrate : (poolGlobal?.sv1_clients?.total_hashrate ?? 0));
// Scope hashrate history to the active pool + mode so stale samples from a
@@ -617,13 +618,15 @@ export function UnifiedDashboard() {
{/* Start Mining Banner (configured but stopped) */}
{!configurationIssue && configuredButStopped && showError && (
-
-
- {autoStarting || isStarting ? (
-
- ) : (
-
- )}
+
+ ) : (
+
+ )}
+ >
+
{autoStarting
? 'Mining services are starting...'
@@ -631,27 +634,26 @@ export function UnifiedDashboard() {
? 'Starting mining services...'
: 'Mining services are stopped.'}
+ {!autoStarting && !isStarting && (
+
+ Start Mining
+
+ )}
- {!autoStarting && !isStarting && (
-
- Start Mining
-
- )}
-
+
)}
{/* Connection Error Banner (not configured or unknown error) */}
{!configurationIssue && (startError || (showError && !configuredButStopped && diagnostics.length === 0)) && (
-
-
-
+
+
{startError || 'Cannot connect to pool. Make sure mining services are running.'}
-
-
+
+
)}
{/* log-derived Diagnostic Banners */}
@@ -661,34 +663,29 @@ export function UnifiedDashboard() {
diagnostic.code === BITCOIN_CORE_DISCONNECTED_CODE;
return (
-
-
-
+
-
{diagnostic.title}
+
{diagnostic.title}
{diagnostic.message}
{diagnostic.recommendation && (
{diagnostic.recommendation}
)}
+ {showBitcoinSetupButton && (
+
window.sessionStorage.setItem(SETUP_TARGET_STEP_STORAGE_KEY, 'bitcoin')}
+ className="inline-flex h-9 shrink-0 items-center justify-center rounded-full bg-red-600 px-4 font-medium text-white transition-colors hover:bg-red-700 sm:ml-4"
+ >
+ Open Bitcoin Setup
+
+ )}
- {showBitcoinSetupButton && (
-
window.sessionStorage.setItem(SETUP_TARGET_STEP_STORAGE_KEY, 'bitcoin')}
- className="inline-flex h-9 shrink-0 items-center justify-center rounded-full bg-red-500 px-4 font-medium text-white transition-colors hover:bg-red-600 sm:ml-4"
- >
- Open Bitcoin Setup
-
- )}
-
+
);
})}
@@ -816,7 +813,7 @@ export function UnifiedDashboard() {
title="Best Difficulty"
value={hasBestDiffSource ? formatDifficulty(bestDiff) : '-'}
/>
-
+
{/* Miner Connection Info */}
@@ -909,4 +906,4 @@ export function UnifiedDashboard() {
)}
);
-}
+}
\ No newline at end of file
From 591394b790e56e0206e14d66c17d07e2f6ff140e Mon Sep 17 00:00:00 2001
From: Ayush
Date: Fri, 7 Aug 2026 21:50:35 +0530
Subject: [PATCH 2/3] Fix icon vertical alignment in Start Mining banner
---
src/pages/UnifiedDashboard.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/UnifiedDashboard.tsx b/src/pages/UnifiedDashboard.tsx
index 6bc8f9a6..3efe3ea0 100644
--- a/src/pages/UnifiedDashboard.tsx
+++ b/src/pages/UnifiedDashboard.tsx
@@ -620,6 +620,7 @@ export function UnifiedDashboard() {
{!configurationIssue && configuredButStopped && showError && (
) : (
From 176860ed2aa5b32b38fd4f1ab4c4048c14b0fb96 Mon Sep 17 00:00:00 2001
From: Ayush
Date: Fri, 7 Aug 2026 21:50:53 +0530
Subject: [PATCH 3/3] Restore isSetupReview state and conditional banner logic
---
src/components/setup/SetupWizard.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/components/setup/SetupWizard.tsx b/src/components/setup/SetupWizard.tsx
index dd31186f..6d13b76b 100644
--- a/src/components/setup/SetupWizard.tsx
+++ b/src/components/setup/SetupWizard.tsx
@@ -90,6 +90,7 @@ export function SetupWizard() {
const [currentStep, setCurrentStep] = useState('mining-mode');
const [data, setData] = useState(initialSetupData);
const [isReconfiguring, setIsReconfiguring] = useState(false);
+ const [isSetupReview, setIsSetupReview] = useState(false);
const [loadingConfig, setLoadingConfig] = useState(true);
const [bitcoinSetupNotice, setBitcoinSetupNotice] = useState(null);
const { results: discoveredNodes, isLoading: isDiscovering, retry: retryDiscovery } = useBitcoinRpcDiscovery();
@@ -110,6 +111,7 @@ export function SetupWizard() {
const setupReviewRequested = window.sessionStorage.getItem(SETUP_REVIEW_STORAGE_KEY) === 'true';
window.sessionStorage.removeItem(SETUP_TARGET_STEP_STORAGE_KEY);
window.sessionStorage.removeItem(SETUP_REVIEW_STORAGE_KEY);
+ setIsSetupReview(setupReviewRequested);
if (targetStep === 'bitcoin' && config.bitcoin) {
nextConfig = {
@@ -290,7 +292,9 @@ export function SetupWizard() {
{isReconfiguring && currentStepIndex === 1 && (
- Reconfiguring SV2 setup — this will replace your current configuration.
+ {isSetupReview
+ ? 'Review your setup to continue mining. Your saved settings are prefilled.'
+ : 'Reconfiguring SV2 setup — this will replace your current configuration.'}
)}
{currentStep === 'template-mode' &&
}