feat: add filter toggle dashboard card (frontend)

**New Components:**
- FiltersConfigCard — Card with toggle switches for all 11 entry filters
- Switch UI component (shadcn/ui with Radix)
- useFilterConfig hook — Fetch & update filters via API
- FilterConfig types

**Features:**
- Live toggle switches for 11 filters (flash_crash, regime, risk, session, signal_combination, h1_bias, time_filter, cooldown, etc.)
- Real-time updates — no page refresh needed
- Shows enabled/disabled count badge
- Filter descriptions + last updated timestamp
- Auto-refresh every 30 seconds
- Refresh button with loading state
- Error handling + retry

**Integration:**
- Added @radix-ui/react-switch dependency
- Added Row 5 to dashboard (grid-cols-2)
- FiltersConfigCard takes left half of new row

**Usage:**
User can toggle any filter ON/OFF from dashboard → API updates filter_config.json → Bot reloads config next loop (live update, no restart)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
GifariKemal
2026-02-09 08:16:21 +07:00
parent d5bfc52447
commit 62c24ccf76
8 changed files with 265 additions and 1 deletions
+3 -1
View File
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
@@ -3515,6 +3516,7 @@
"version": "19.2.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3524,7 +3526,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
+1
View File
@@ -10,6 +10,7 @@
},
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"class-variance-authority": "^0.7.1",
+11
View File
@@ -18,6 +18,7 @@ import {
EntryFilterCard,
PerformanceCard,
ModelCard,
FiltersConfigCard,
} from "@/components/dashboard";
import { Skeleton } from "@/components/ui/skeleton";
import { TooltipProvider } from "@/components/ui/tooltip";
@@ -235,6 +236,16 @@ export default function Dashboard() {
<LogCard logs={data.logs} />
</div>
</div>
{/* Row 5: Filter Controls */}
<div className="flex-[1.4] min-h-0 grid grid-cols-2 gap-1.5">
<div className={cn("stagger-enter h-full", visible[15] && "visible")}>
<FiltersConfigCard />
</div>
<div className={cn("stagger-enter h-full", visible[16] && "visible")}>
{/* Placeholder for future card */}
</div>
</div>
</main>
</div>
</TooltipProvider>
@@ -0,0 +1,128 @@
"use client";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { useFilterConfig } from "@/hooks/use-filter-config";
import { AlertCircle, CheckCircle2, RefreshCcw } from "lucide-react";
export function FiltersConfigCard() {
const { config, loading, error, updating, toggleFilter, refresh } = useFilterConfig();
if (loading && !config) {
return (
<Card className="glass">
<CardHeader>
<CardTitle>Entry Filters</CardTitle>
<CardDescription>Loading...</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center justify-between">
<Skeleton className="h-4 w-[200px]" />
<Skeleton className="h-5 w-9 rounded-full" />
</div>
))}
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="glass border-red-500/20">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-red-400">
<AlertCircle className="h-5 w-5" />
Entry Filters
</CardTitle>
<CardDescription className="text-red-400/70">{error}</CardDescription>
</CardHeader>
<CardContent>
<button
onClick={refresh}
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<RefreshCcw className="h-4 w-4" />
Retry
</button>
</CardContent>
</Card>
);
}
if (!config) return null;
const filters = Object.entries(config.filters);
const enabledCount = filters.filter(([, f]) => f.enabled).length;
// Sort by name for consistent display
const sortedFilters = filters.sort((a, b) => a[1].name.localeCompare(b[1].name));
return (
<Card className="glass">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
Entry Filters
<Badge variant="outline" className="font-normal">
{enabledCount}/{filters.length}
</Badge>
</CardTitle>
<CardDescription>
Toggle filters on/off updates live without bot restart
</CardDescription>
</div>
<button
onClick={refresh}
disabled={updating}
className="p-2 hover:bg-white/5 rounded-lg transition-colors disabled:opacity-50"
title="Refresh"
>
<RefreshCcw className={`h-4 w-4 ${updating ? 'animate-spin' : ''}`} />
</button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{sortedFilters.map(([key, filter]) => (
<div
key={key}
className="flex items-start justify-between gap-4 py-2 border-b border-white/5 last:border-0"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{filter.name}</span>
{filter.enabled ? (
<CheckCircle2 className="h-3.5 w-3.5 text-green-400 shrink-0" />
) : (
<AlertCircle className="h-3.5 w-3.5 text-orange-400 shrink-0" />
)}
</div>
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
{filter.description}
</p>
</div>
<Switch
checked={filter.enabled}
onCheckedChange={() => toggleFilter(key)}
disabled={updating}
className="shrink-0"
/>
</div>
))}
{config.metadata?.updated_at && (
<div className="text-xs text-muted-foreground pt-2 border-t border-white/5">
Last updated: {new Date(config.metadata.updated_at).toLocaleString('id-ID', {
timeZone: 'Asia/Jakarta',
dateStyle: 'short',
timeStyle: 'short'
})} WIB
</div>
)}
</CardContent>
</Card>
);
}
@@ -16,3 +16,4 @@ export { EntryFilterCard } from "./entry-filter-card";
export { PerformanceCard } from "./performance-card";
export { ModelCard } from "./model-card";
export { ModelDialog } from "./model-dialog";
export { FiltersConfigCard } from "./filters-config-card";
@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
@@ -0,0 +1,75 @@
import { useState, useEffect } from 'react';
import type { FilterConfigData, FilterUpdate } from '@/types/filters';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8001';
export function useFilterConfig() {
const [config, setConfig] = useState<FilterConfigData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [updating, setUpdating] = useState(false);
const fetchConfig = async () => {
try {
setLoading(true);
const response = await fetch(`${API_URL}/api/filters/config`);
if (!response.ok) throw new Error('Failed to fetch filter config');
const data = await response.json();
setConfig(data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
const updateFilters = async (updates: FilterUpdate) => {
try {
setUpdating(true);
const response = await fetch(`${API_URL}/api/filters/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
if (!response.ok) throw new Error('Failed to update filters');
const result = await response.json();
if (result.success) {
// Refresh config after successful update
await fetchConfig();
}
return result;
} catch (err) {
setError(err instanceof Error ? err.message : 'Update failed');
throw err;
} finally {
setUpdating(false);
}
};
const toggleFilter = async (filterKey: string) => {
if (!config) return;
const currentState = config.filters[filterKey]?.enabled ?? true;
await updateFilters({ [filterKey]: !currentState });
};
useEffect(() => {
fetchConfig();
// Refresh every 30 seconds
const interval = setInterval(fetchConfig, 30000);
return () => clearInterval(interval);
}, []);
return {
config,
loading,
error,
updating,
toggleFilter,
updateFilters,
refresh: fetchConfig,
};
}
+17
View File
@@ -0,0 +1,17 @@
export interface FilterConfig {
enabled: boolean;
name: string;
description: string;
}
export interface FilterConfigData {
filters: Record<string, FilterConfig>;
metadata: {
updated_at: string;
version: string;
};
}
export interface FilterUpdate {
[key: string]: boolean;
}