+
Audit Log
@@ -410,109 +461,153 @@ export default function AuditLog() {
setFilters(f)} />
-
-
-
- {filteredLogs.length} entries
- {logs.length} total
-
- {isLoading ? (
-
- Loading audit log...
-
- ) : filteredLogs.length === 0 ? (
-
- {logs.length === 0 ? 'No audit entries yet.' : 'No entries match the current filters.'}
+
+
+ {filteredLogs.length} entries
+ {logs.length} total
- ) : (
- filteredLogs.map((entry: AuditEntry) => (
- setSelectedId(entry.id)}
- style={{
- display: 'grid',
- gridTemplateColumns: auditPresets.visibleColumns.map((id) => {
- const col = AUDIT_LOG_COLUMNS.find((c) => c.id === id);
- return col?.width || '1fr';
- }).join(' '),
- gap: '12px',
- padding: auditPresets.density === 'compact' ? '8px 12px' : auditPresets.density === 'comfortable' ? '10px 14px' : '12px 16px',
- borderBottom: '1px solid var(--border)',
- cursor: 'pointer',
- background: selectedId === entry.id ? 'var(--cyan-glow-sm)' : 'transparent',
- transition: 'var(--transition)',
- alignItems: 'center',
- fontSize: auditPresets.density === 'compact' ? '11px' : auditPresets.density === 'comfortable' ? '12px' : '13px',
- }}
- >
- {auditPresets.visibleColumns.includes('severity') && (
-
- )}
- {auditPresets.visibleColumns.includes('category') && (
-
- {entry.category}
-
- )}
- {auditPresets.visibleColumns.includes('action') && (
-
- {entry.action}
-
- )}
- {auditPresets.visibleColumns.includes('timestamp') && (
-
- {formatDistanceToNow(new Date(entry.timestamp), { addSuffix: true })}
-
- )}
+ {isLoading ? (
+
+ Loading audit log...
+
+ ) : filteredLogs.length === 0 ? (
+
+ {logs.length === 0
+ ? 'No audit entries yet.'
+ : 'No entries match the current filters.'}
- ))
- )}
-
-
-
+ ) : (
+ filteredLogs.map((entry: AuditEntry) => (
+
setSelectedId(entry.id)}
+ style={{
+ display: 'grid',
+ gridTemplateColumns: auditPresets.visibleColumns
+ .map((id) => {
+ const col = AUDIT_LOG_COLUMNS.find((c) => c.id === id);
+ return col?.width || '1fr';
+ })
+ .join(' '),
+ gap: '12px',
+ padding:
+ auditPresets.density === 'compact'
+ ? '8px 12px'
+ : auditPresets.density === 'comfortable'
+ ? '10px 14px'
+ : '12px 16px',
+ borderBottom: '1px solid var(--border)',
+ cursor: 'pointer',
+ background: selectedId === entry.id ? 'var(--cyan-glow-sm)' : 'transparent',
+ transition: 'var(--transition)',
+ alignItems: 'center',
+ fontSize:
+ auditPresets.density === 'compact'
+ ? '11px'
+ : auditPresets.density === 'comfortable'
+ ? '12px'
+ : '13px',
+ }}
+ >
+ {auditPresets.visibleColumns.includes('severity') && (
+
+ )}
+ {auditPresets.visibleColumns.includes('category') && (
+
+ {entry.category}
+
+ )}
+ {auditPresets.visibleColumns.includes('action') && (
+
+ {entry.action}
+
+ )}
+ {auditPresets.visibleColumns.includes('timestamp') && (
+
+ {formatDistanceToNow(new Date(entry.timestamp), { addSuffix: true })}
+
+ )}
+
+ ))
+ )}
+
+
+
{selectedEntry ? (
-
+
Entry Details
@@ -533,52 +628,120 @@ export default function AuditLog() {
-
Severity
+
+ Severity
+
-
Category
-
{selectedEntry.category}
+
+ Category
+
+
+ {selectedEntry.category}
+
-
Timestamp
-
+
+ Timestamp
+
+
{new Date(selectedEntry.timestamp).toLocaleString()}
-
Message
-
- {selectedEntry.message}
-
-
- {selectedEntry.metadata && Object.keys(selectedEntry.metadata).length > 0 && (
-
+
+ }}
+ >
+ {selectedEntry.action}
+
+
+ {selectedEntry.metadata && Object.keys(selectedEntry.metadata).length > 0 && (
+
+
+ Metadata
+
+
{JSON.stringify(selectedEntry.metadata, null, 2)}
@@ -586,8 +749,18 @@ export default function AuditLog() {
) : (
-
-
{'\uD83D\uDCCB'}
+
+
📋
Select an entry to view details
)}
diff --git a/src/components/dashboard/FeatureFlags.tsx b/src/components/dashboard/FeatureFlags.tsx
index 7f68e37e..e8f1235c 100644
--- a/src/components/dashboard/FeatureFlags.tsx
+++ b/src/components/dashboard/FeatureFlags.tsx
@@ -13,12 +13,7 @@ import {
type FeatureFlagStatus,
} from '../../lib/featureFlagLifecycle';
-const ENVIRONMENTS: FeatureFlagEnvironment[] = [
- 'development',
- 'staging',
- 'production',
- 'test',
-];
+const ENVIRONMENTS: FeatureFlagEnvironment[] = ['development', 'staging', 'production', 'test'];
const STATUS_COLOR: Record
= {
draft: 'var(--text-muted)',
@@ -80,6 +75,69 @@ export default function FeatureFlags() {
});
const [message, setMessage] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const [evalResult, setEvalResult] = useState('');
+ const [remoteAIEnabled, setRemoteAIEnabled] = useState(null);
+ const [operatorToken, setOperatorToken] = useState('');
+ const [aiReason, setAIReason] = useState('');
+ const [aiControlBusy, setAIControlBusy] = useState(false);
+ const [aiControlMessage, setAIControlMessage] = useState('');
+ const [remoteAudit, setRemoteAudit] = useState<
+ Array<{
+ enabled: boolean;
+ previousEnabled: boolean;
+ updatedAt: string;
+ actor: string;
+ reason: string;
+ }>
+ >([]);
+
+ const refreshAIControl = useCallback(async () => {
+ try {
+ const [statusResponse, auditResponse] = await Promise.all([
+ fetch('/api/v1/ai-controls', { cache: 'no-store' }),
+ fetch('/api/v1/ai-controls/audit', { cache: 'no-store' }),
+ ]);
+ if (!statusResponse.ok || !auditResponse.ok)
+ throw new Error('Runtime controls unavailable; check API and shared-store configuration.');
+ const status = await statusResponse.json();
+ const auditPayload = await auditResponse.json();
+ if (typeof status?.data?.enabled !== 'boolean' || !Array.isArray(auditPayload?.data))
+ throw new Error('Invalid runtime control response.');
+ setRemoteAIEnabled(status.data.enabled);
+ setRemoteAudit(auditPayload.data);
+ setAIControlMessage('');
+ } catch (error) {
+ setRemoteAIEnabled(null);
+ setAIControlMessage(error instanceof Error ? error.message : 'Runtime controls unavailable.');
+ }
+ }, []);
+
+ React.useEffect(() => {
+ void refreshAIControl();
+ }, [refreshAIControl]);
+
+ const setGlobalAIEnabled = async (enabled: boolean) => {
+ setAIControlBusy(true);
+ setAIControlMessage('');
+ try {
+ const response = await fetch('/api/v1/ai-controls', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${operatorToken}` },
+ body: JSON.stringify({ enabled, reason: aiReason }),
+ });
+ const payload = await response.json();
+ if (!response.ok)
+ throw new Error(payload?.message || `Control update failed (${response.status}).`);
+ setRemoteAIEnabled(payload.data.enabled);
+ setOperatorToken('');
+ setAIReason('');
+ setAIControlMessage(`Global AI panels ${enabled ? 'enabled' : 'disabled'}.`);
+ await refreshAIControl();
+ } catch (error) {
+ setAIControlMessage(error instanceof Error ? error.message : 'Control update failed.');
+ } finally {
+ setAIControlBusy(false);
+ }
+ };
const inputStyle: React.CSSProperties = {
background: 'var(--bg-input)',
@@ -143,7 +201,7 @@ export default function FeatureFlags() {
.map((s) => s.trim())
.filter(Boolean),
},
- 'dashboard-ui',
+ 'dashboard-ui'
);
setMessage({ type: 'ok', text: `Created draft flag "${flag.key}"` });
setForm({
@@ -182,7 +240,7 @@ export default function FeatureFlags() {
});
setEvalResult(
`${result.enabled ? 'ENABLED' : 'DISABLED'} — ${result.reason}` +
- (result.flag ? ` [${result.flag.status}]` : ''),
+ (result.flag ? ` [${result.flag.status}]` : '')
);
refresh();
} catch (err) {
@@ -210,6 +268,108 @@ export default function FeatureFlags() {
+
+
+
+ {remoteAIEnabled === null
+ ? 'Status unavailable (panels fail closed)'
+ : `AI panels ${remoteAIEnabled ? 'enabled' : 'disabled'}`}
+
+
+ This server-controlled switch applies globally without redeploying. Enter the configured
+ operator token to change it; credentials are held only in this form and are never
+ stored.
+
+
setOperatorToken(event.target.value)}
+ />
+
setAIReason(event.target.value)}
+ />
+
+
+
+
+
+ {aiControlMessage && (
+
+ {aiControlMessage}
+
+ )}
+ {remoteAudit.length > 0 && (
+
+
Recent global switch changes
+ {remoteAudit.slice(0, 10).map((entry, index) => (
+
+ {new Date(entry.updatedAt).toLocaleString()} ·{' '}
+ {entry.previousEnabled ? 'enabled' : 'disabled'} →{' '}
+ {entry.enabled ? 'enabled' : 'disabled'} · {entry.actor}
+ {entry.reason ? ` · ${entry.reason}` : ''}
+
+ ))}
+
+ )}
+
+
+
+
+
{message && (
diff --git a/src/components/dashboard/LiquidityPredictionDashboard.tsx b/src/components/dashboard/LiquidityPredictionDashboard.tsx
index 02f5efbb..bdfa2602 100644
--- a/src/components/dashboard/LiquidityPredictionDashboard.tsx
+++ b/src/components/dashboard/LiquidityPredictionDashboard.tsx
@@ -1,21 +1,13 @@
-import React, { useEffect, useState, useMemo } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import {
Brain,
TrendingUp,
- AlertTriangle,
Zap,
RefreshCw,
Sliders,
CheckCircle2,
ShieldAlert,
- ArrowRightLeft,
- Clock,
- Layers,
Activity,
- Maximize2,
- ArrowUpRight,
- ArrowDownRight,
- Info,
} from 'lucide-react';
import {
ResponsiveContainer,
@@ -26,9 +18,9 @@ import {
Tooltip,
CartesianGrid,
Legend,
- ReferenceLine,
} from 'recharts';
import { useStore } from '../../lib/store';
+import { useAIKillSwitch } from '../../context/AIKillSwitchContext';
import {
predictLiquidityFlow,
PredictionResult,
@@ -36,6 +28,7 @@ import {
} from '../../ml/liquidityPredictionModel';
export default function LiquidityPredictionDashboard() {
+ const { enabled, ready } = useAIKillSwitch();
const { network } = useStore();
const [sellingAsset, setSellingAsset] = useState
('native');
const [buyingAsset, setBuyingAsset] = useState(
@@ -49,42 +42,44 @@ export default function LiquidityPredictionDashboard() {
const [selectedStrategy, setSelectedStrategy] = useState(null);
const [appliedNotice, setAppliedNotice] = useState(null);
- async function fetchPrediction() {
+ const fetchPrediction = useCallback(async () => {
+ if (!enabled || !ready) return;
setLoading(true);
try {
const result = await predictLiquidityFlow(sellingAsset, buyingAsset, {
horizonHours,
tradeAmount,
- network,
+ network: network.networkId,
});
setData(result);
- if (result.recommendations.length > 0 && !selectedStrategy) {
- setSelectedStrategy(result.recommendations[0]);
+ const firstRecommendation = result.recommendations[0];
+ if (firstRecommendation) {
+ setSelectedStrategy((current) => current ?? firstRecommendation);
}
} catch (err) {
console.error('Failed to load liquidity prediction:', err);
} finally {
setLoading(false);
}
- }
+ }, [enabled, ready, sellingAsset, buyingAsset, horizonHours, tradeAmount, network.networkId]);
useEffect(() => {
- fetchPrediction();
- }, [sellingAsset, buyingAsset, horizonHours, tradeAmount, network]);
+ if (enabled && ready) fetchPrediction();
+ }, [enabled, ready, fetchPrediction]);
useEffect(() => {
- if (!autoRefresh) return;
+ if (!autoRefresh || !enabled || !ready) return;
const interval = setInterval(() => {
fetchPrediction();
}, 15000);
return () => clearInterval(interval);
- }, [autoRefresh, sellingAsset, buyingAsset, horizonHours, tradeAmount, network]);
+ }, [autoRefresh, enabled, ready, fetchPrediction]);
+
+ if (!enabled || !ready) return null;
const applyStrategy = (strategy: TradingRecommendation) => {
setSelectedStrategy(strategy);
- setAppliedNotice(
- `Applied Strategy: ${strategy.title}. Parameters updated for DEX execution.`
- );
+ setAppliedNotice(`Applied Strategy: ${strategy.title}. Parameters updated for DEX execution.`);
setTimeout(() => setAppliedNotice(null), 4000);
};
@@ -221,7 +216,9 @@ export default function LiquidityPredictionDashboard() {
}}
>