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;
+}