From 62c24ccf765a8adfa9d428a3a0f208a2f8324886 Mon Sep 17 00:00:00 2001 From: GifariKemal Date: Mon, 9 Feb 2026 08:16:21 +0700 Subject: [PATCH] feat: add filter toggle dashboard card (frontend) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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 --- web-dashboard/package-lock.json | 4 +- web-dashboard/package.json | 1 + web-dashboard/src/app/page.tsx | 11 ++ .../dashboard/filters-config-card.tsx | 128 ++++++++++++++++++ .../src/components/dashboard/index.ts | 1 + web-dashboard/src/components/ui/switch.tsx | 29 ++++ web-dashboard/src/hooks/use-filter-config.ts | 75 ++++++++++ web-dashboard/src/types/filters.ts | 17 +++ 8 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 web-dashboard/src/components/dashboard/filters-config-card.tsx create mode 100644 web-dashboard/src/components/ui/switch.tsx create mode 100644 web-dashboard/src/hooks/use-filter-config.ts create mode 100644 web-dashboard/src/types/filters.ts diff --git a/web-dashboard/package-lock.json b/web-dashboard/package-lock.json index d773ea0..b6a26b5 100644 --- a/web-dashboard/package-lock.json +++ b/web-dashboard/package-lock.json @@ -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" diff --git a/web-dashboard/package.json b/web-dashboard/package.json index 8f1cdb0..2926b23 100644 --- a/web-dashboard/package.json +++ b/web-dashboard/package.json @@ -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", diff --git a/web-dashboard/src/app/page.tsx b/web-dashboard/src/app/page.tsx index 1e44950..985d63a 100644 --- a/web-dashboard/src/app/page.tsx +++ b/web-dashboard/src/app/page.tsx @@ -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() { + + {/* Row 5: Filter Controls */} +
+
+ +
+
+ {/* Placeholder for future card */} +
+
diff --git a/web-dashboard/src/components/dashboard/filters-config-card.tsx b/web-dashboard/src/components/dashboard/filters-config-card.tsx new file mode 100644 index 0000000..16c3c10 --- /dev/null +++ b/web-dashboard/src/components/dashboard/filters-config-card.tsx @@ -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 ( + + + Entry Filters + Loading... + + + {[1, 2, 3, 4, 5].map((i) => ( +
+ + +
+ ))} +
+
+ ); + } + + if (error) { + return ( + + + + + Entry Filters + + {error} + + + + + + ); + } + + 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 ( + + +
+
+ + Entry Filters + + {enabledCount}/{filters.length} + + + + Toggle filters on/off — updates live without bot restart + +
+ +
+
+ + {sortedFilters.map(([key, filter]) => ( +
+
+
+ {filter.name} + {filter.enabled ? ( + + ) : ( + + )} +
+

+ {filter.description} +

+
+ toggleFilter(key)} + disabled={updating} + className="shrink-0" + /> +
+ ))} + + {config.metadata?.updated_at && ( +
+ Last updated: {new Date(config.metadata.updated_at).toLocaleString('id-ID', { + timeZone: 'Asia/Jakarta', + dateStyle: 'short', + timeStyle: 'short' + })} WIB +
+ )} +
+
+ ); +} diff --git a/web-dashboard/src/components/dashboard/index.ts b/web-dashboard/src/components/dashboard/index.ts index db20938..3b815a7 100644 --- a/web-dashboard/src/components/dashboard/index.ts +++ b/web-dashboard/src/components/dashboard/index.ts @@ -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"; diff --git a/web-dashboard/src/components/ui/switch.tsx b/web-dashboard/src/components/ui/switch.tsx new file mode 100644 index 0000000..5f4117f --- /dev/null +++ b/web-dashboard/src/components/ui/switch.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/web-dashboard/src/hooks/use-filter-config.ts b/web-dashboard/src/hooks/use-filter-config.ts new file mode 100644 index 0000000..d21adc0 --- /dev/null +++ b/web-dashboard/src/hooks/use-filter-config.ts @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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, + }; +} diff --git a/web-dashboard/src/types/filters.ts b/web-dashboard/src/types/filters.ts new file mode 100644 index 0000000..b4f8419 --- /dev/null +++ b/web-dashboard/src/types/filters.ts @@ -0,0 +1,17 @@ +export interface FilterConfig { + enabled: boolean; + name: string; + description: string; +} + +export interface FilterConfigData { + filters: Record; + metadata: { + updated_at: string; + version: string; + }; +} + +export interface FilterUpdate { + [key: string]: boolean; +}