diff --git a/data/filter_config.json b/data/filter_config.json index b529c74..d0a2a00 100644 --- a/data/filter_config.json +++ b/data/filter_config.json @@ -57,7 +57,7 @@ } }, "metadata": { - "updated_at": "2026-02-09T08:05:00+07:00", + "updated_at": "2026-02-09T08:28:17.078826+07:00", "version": "1.0" } -} +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 5dfec04..c098257 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,8 +39,8 @@ services: ports: - "${API_PORT:-8000}:8000" volumes: - # Mount data/ so API can read bot_status.json written by the bot on host - - ./data:/app/data:ro + # Mount data/ for reading bot_status.json and writing filter_config.json + - ./data:/app/data depends_on: postgres: condition: service_healthy diff --git a/src/smart_risk_manager.py b/src/smart_risk_manager.py index 1f808a1..0b6bb6e 100644 --- a/src/smart_risk_manager.py +++ b/src/smart_risk_manager.py @@ -686,13 +686,22 @@ class SmartRiskManager: current_hour = now.hour # Early cut: If loss > 30% of max and momentum negative, cut early + # GRACE PERIOD: Wait at least 1 M15 candle (15 min) before early cut + # Intra-candle moves are noise — let the trade develop on its timeframe + trade_age_seconds = (now - guard.entry_time).total_seconds() + trade_age_minutes = trade_age_seconds / 60 + if current_profit < 0: loss_percent_of_max = abs(current_profit) / self.max_loss_per_trade * 100 # Cut early if momentum is against us AND loss is significant + # BUT only after grace period (15 min = 1 M15 candle) if momentum < -50 and loss_percent_of_max >= 30: # #24B: relaxed from -30 (backtest +$125) - logger.info(f"[EARLY CUT] Loss ${abs(current_profit):.2f} ({loss_percent_of_max:.0f}%) + weak momentum ({momentum:.0f}) - CUTTING EARLY") - return True, ExitReason.TREND_REVERSAL, f"[EARLY CUT] Loss ${abs(current_profit):.2f} + momentum {momentum:.0f} - cutting to preserve daily limit" + if trade_age_minutes < 15: + logger.info(f"[GRACE] Loss ${abs(current_profit):.2f} ({loss_percent_of_max:.0f}%) + momentum ({momentum:.0f}) — holding {trade_age_minutes:.1f}m/{15}m grace period") + else: + logger.info(f"[EARLY CUT] Loss ${abs(current_profit):.2f} ({loss_percent_of_max:.0f}%) + weak momentum ({momentum:.0f}) - CUTTING EARLY (age: {trade_age_minutes:.0f}m)") + return True, ExitReason.TREND_REVERSAL, f"[EARLY CUT] Loss ${abs(current_profit):.2f} + momentum {momentum:.0f} - cutting to preserve daily limit" # NOTE: Smart Hold REMOVED - no more holding losers hoping for golden time # If SL is hit, close the trade immediately diff --git a/web-dashboard/src/app/page.tsx b/web-dashboard/src/app/page.tsx index 985d63a..22ee563 100644 --- a/web-dashboard/src/app/page.tsx +++ b/web-dashboard/src/app/page.tsx @@ -13,12 +13,11 @@ import { PositionsCard, LogCard, PriceChart, - EquityChart, BotStatusCard, EntryFilterCard, PerformanceCard, ModelCard, - FiltersConfigCard, + AssistantCard, } from "@/components/dashboard"; import { Skeleton } from "@/components/ui/skeleton"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -69,7 +68,7 @@ function ErrorDisplay({ message }: { message: string }) { export default function Dashboard() { const { data, loading, error, dataAge } = useTradingData(); - const visible = useStaggerEntry(17, 40); + const visible = useStaggerEntry(15, 40); const now = new Date(); const wibTime = now.toLocaleTimeString("en-US", { @@ -204,10 +203,7 @@ export default function Dashboard() {
- +
@@ -237,15 +233,6 @@ export default function Dashboard() { - {/* Row 5: Filter Controls */} -
-
- -
-
- {/* Placeholder for future card */} -
-
diff --git a/web-dashboard/src/components/dashboard/assistant-card.tsx b/web-dashboard/src/components/dashboard/assistant-card.tsx new file mode 100644 index 0000000..9177ded --- /dev/null +++ b/web-dashboard/src/components/dashboard/assistant-card.tsx @@ -0,0 +1,404 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Bot, TrendingUp, TrendingDown, ShieldAlert, Clock, Zap, + AlertTriangle, Coffee, CheckCircle2, XCircle, Minus, + Activity, Target, BarChart3, Eye, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { TradingStatus } from "@/types/trading"; + +type InsightType = "info" | "success" | "warning" | "danger"; + +interface Insight { + icon: React.ReactNode; + text: string; + type: InsightType; +} + +const typeStyles: Record = { + info: "text-blue-400/90", + success: "text-apple-green", + warning: "text-orange-400", + danger: "text-apple-red", +}; + +const sessionNames: Record = { + sydney: "Sydney", tokyo: "Tokyo", london: "London", + new_york: "New York", off_hours: "Off Hours", +}; + +function generateInsights(data: TradingStatus): Insight[] { + const insights: Insight[] = []; + const hasPositions = data.positions && data.positions.length > 0; + const sessionName = sessionNames[data.session?.toLowerCase()] || data.session; + const regimeName = data.regime?.name || "unknown"; + const regimeConf = data.regime?.confidence ? (data.regime.confidence * 100).toFixed(0) : "?"; + const smcSignal = data.smc?.signal || "NONE"; + const smcConf = data.smc?.confidence ? (data.smc.confidence * 100).toFixed(0) : "0"; + const mlSignal = data.ml?.signal || "HOLD"; + const mlConf = data.ml?.confidence ? (data.ml.confidence * 100).toFixed(0) : "0"; + const h1Bias = data.h1Bias || "N/A"; + const dynThreshold = data.dynamicThreshold ? (data.dynamicThreshold * 100).toFixed(0) : "60"; + const spread = data.spread?.toFixed(1) || "?"; + + // ═══════════════════════════════════════════ + // 1. STATUS MARKET + // ═══════════════════════════════════════════ + if (data.marketClose && !data.marketClose.marketOpen) { + insights.push({ + icon: , + text: "Market sedang tutup. Bot dalam mode standby — tidak ada analisis atau eksekusi. Menunggu market buka kembali.", + type: "info", + }); + return insights; + } + + if (data.marketClose?.nearWeekend) { + const hrs = data.marketClose.hoursToWeekendClose; + insights.push({ + icon: , + text: `Peringatan: Weekend close dalam ${hrs.toFixed(1)} jam (Sabtu 05:00 WIB). Bot akan menolak entry baru dan memastikan semua posisi ditutup sebelum market close.`, + type: "warning", + }); + } + + // ═══════════════════════════════════════════ + // 2. RANGKUMAN SITUASI (top-level summary) + // ═══════════════════════════════════════════ + if (hasPositions) { + const totalProfit = data.positions.reduce((sum, p) => sum + p.profit, 0); + const profitStr = totalProfit >= 0 ? `+$${totalProfit.toFixed(2)}` : `-$${Math.abs(totalProfit).toFixed(2)}`; + insights.push({ + icon: , + text: `Sedang dalam posisi (${data.positions.length} trade aktif, total P/L: ${profitStr}). Bot memantau exit conditions setiap 5 detik.`, + type: totalProfit >= 0 ? "success" : "warning", + }); + } else { + insights.push({ + icon: , + text: `Tidak ada posisi terbuka. Bot menganalisis market setiap candle M15 untuk mencari peluang entry.`, + type: "info", + }); + } + + // ═══════════════════════════════════════════ + // 3. SESI & WAKTU + // ═══════════════════════════════════════════ + if (data.isGoldenTime) { + insights.push({ + icon: , + text: `Sesi ${sessionName} — GOLDEN TIME! Overlap London-NY menghasilkan volatilitas tertinggi. Lot size dinaikkan ${data.sessionMultiplier || 1}x. Peluang terbaik untuk entry.`, + type: "success", + }); + } else if (!data.canTrade) { + const nextSession = data.session?.toLowerCase() === "off_hours" ? "Sydney (04:00 WIB)" + : data.session?.toLowerCase() === "sydney" ? "London (14:00 WIB)" + : "sesi berikutnya"; + insights.push({ + icon: , + text: `Sesi ${sessionName} — di luar jam trading aktif. Volume rendah, spread bisa melebar. Menunggu ${nextSession}.`, + type: "info", + }); + } else { + const multiplierText = data.sessionMultiplier && data.sessionMultiplier > 1 + ? ` Lot multiplier: ${data.sessionMultiplier}x.` + : data.sessionMultiplier && data.sessionMultiplier < 1 + ? ` SAFE MODE: lot dikurangi ${data.sessionMultiplier}x.` + : ""; + insights.push({ + icon: , + text: `Sesi ${sessionName} aktif — market terbuka untuk trading.${multiplierText}`, + type: "info", + }); + } + + if (data.timeFilter?.isBlocked) { + insights.push({ + icon: , + text: `Jam ${data.timeFilter.wibHour}:00 WIB diblokir oleh time filter (jam-jam dengan win rate rendah secara historis). Entry ditunda.`, + type: "warning", + }); + } + + // ═══════════════════════════════════════════ + // 4. KONDISI MARKET (regime + spread + H1) + // ═══════════════════════════════════════════ + const regimeDetail = regimeName.includes("trending") + ? `Market trending (HMM confidence ${regimeConf}%) — kondisi ideal. Harga bergerak terarah, sinyal lebih reliable.` + : regimeName.includes("high_volatility") || regimeName.includes("volatile") + ? `Volatilitas tinggi (HMM ${regimeConf}%) — HATI-HATI! Pergerakan harga liar, SL bisa cepat tersentuh. Bot menaikkan spread tolerance.` + : regimeName.includes("low_volatility") || regimeName.includes("ranging") + ? `Volatilitas rendah / ranging (HMM ${regimeConf}%) — market diam, peluang kecil. Bot menunggu breakout atau perubahan regime.` + : `Regime: ${regimeName} (HMM ${regimeConf}%).`; + + insights.push({ + icon: regimeName.includes("trending") ? + : regimeName.includes("volatile") || regimeName.includes("high") ? + : , + text: regimeDetail, + type: regimeName.includes("trending") ? "success" + : regimeName.includes("volatile") || regimeName.includes("high") ? "warning" + : "info", + }); + + // H1 Bias detail + if (h1Bias && h1Bias !== "N/A") { + const h1Text = h1Bias === "BULLISH" + ? `H1 Bias: BULLISH — harga di atas EMA20 H1, tren naik jangka menengah. Entry BUY diizinkan.` + : h1Bias === "BEARISH" + ? `H1 Bias: BEARISH — harga di bawah EMA20 H1, tren turun jangka menengah. Entry SELL diizinkan.` + : `H1 Bias: NEUTRAL — harga dekat EMA20 H1, tidak ada tren jelas. Entry di-hold sampai arah terbentuk.`; + insights.push({ + icon: h1Bias === "BULLISH" ? + : h1Bias === "BEARISH" ? + : , + text: h1Text, + type: h1Bias === "NEUTRAL" ? "warning" : "info", + }); + } + + // Spread + insights.push({ + icon: , + text: `Spread saat ini: ${spread} pips. ${parseFloat(spread) > 40 ? "Cukup lebar — entry mungkin ditunda." : parseFloat(spread) > 25 ? "Normal." : "Ketat — kondisi bagus."}`, + type: parseFloat(spread) > 40 ? "warning" : "info", + }); + + // ═══════════════════════════════════════════ + // 5. ANALISIS SINYAL + // ═══════════════════════════════════════════ + if (smcSignal !== "NONE" && smcSignal !== "HOLD") { + const smcReason = data.smc?.reason || ""; + const mlAgrees = mlSignal === smcSignal; + const mlAboveThreshold = data.ml?.confidence && data.ml.confidence * 100 >= parseFloat(dynThreshold); + + if (mlAgrees && mlAboveThreshold) { + insights.push({ + icon: , + text: `SINYAL KUAT: SMC ${smcSignal} (${smcConf}%) + ML ${mlSignal} (${mlConf}%) — keduanya sepakat dan di atas threshold ${dynThreshold}%.${smcReason ? ` SMC: ${smcReason}.` : ""} Tinggal filter lain terpenuhi untuk entry.`, + type: "success", + }); + } else if (mlAgrees && !mlAboveThreshold) { + insights.push({ + icon: , + text: `SMC ${smcSignal} (${smcConf}%) & ML setuju ${mlSignal}, tapi confidence ML (${mlConf}%) masih di bawah threshold (${dynThreshold}%). Perlu lebih yakin.`, + type: "warning", + }); + } else { + insights.push({ + icon: , + text: `Sinyal konflik — SMC: ${smcSignal} (${smcConf}%) vs ML: ${mlSignal} (${mlConf}%). Bot menunggu kedua model sinkron sebelum entry.${smcReason ? ` SMC: ${smcReason}.` : ""}`, + type: "info", + }); + } + } else { + insights.push({ + icon: , + text: `Belum ada sinyal — SMC: ${smcSignal}, ML: ${mlSignal} (${mlConf}%). Menunggu setup terbentuk di candle M15 berikutnya.`, + type: "info", + }); + } + + // ═══════════════════════════════════════════ + // 6. POSISI TERBUKA (detail) + // ═══════════════════════════════════════════ + if (hasPositions) { + for (const pos of data.positions) { + const detail = data.positionDetails?.find((d) => d.ticket === pos.ticket); + const profitStr = pos.profit >= 0 ? `+$${pos.profit.toFixed(2)}` : `-$${Math.abs(pos.profit).toFixed(2)}`; + const dir = pos.type; + + const ageMinutes = detail?.tradeHours ? detail.tradeHours * 60 : 0; + const ageText = ageMinutes > 0 + ? ageMinutes < 60 ? `${ageMinutes.toFixed(0)}m` : `${(ageMinutes / 60).toFixed(1)}j` + : ""; + const momentumVal = detail?.momentum ?? 0; + const tpProb = detail?.tpProbability ?? 0; + const peakProfit = detail?.peakProfit ?? 0; + const drawdown = detail?.drawdownFromPeak ?? 0; + const reversalWarns = detail?.reversalWarnings ?? 0; + + let analysis = ""; + if (pos.profit >= 20) { + analysis = `Profit sangat baik! Trailing SL aktif mengunci keuntungan. Peak: $${peakProfit.toFixed(2)}, drawdown dari peak: $${drawdown.toFixed(2)}. TP probability: ${tpProb.toFixed(0)}%.`; + } else if (pos.profit >= 10) { + analysis = `Profit bagus — momentum ${momentumVal > 0 ? "positif" : "melemah"} (${momentumVal.toFixed(0)}). Peak profit: $${peakProfit.toFixed(2)}. ${tpProb > 50 ? "Peluang capai TP masih tinggi." : "Mulai pantau untuk ambil profit."}`; + } else if (pos.profit >= 0) { + analysis = `Masih floating ${profitStr} (${ageText}). Momentum: ${momentumVal.toFixed(0)}. ${ageMinutes < 15 ? "Masih dalam grace period 15 menit — biarkan trade berkembang." : "Memantau arah selanjutnya."}`; + } else if (ageMinutes < 15) { + analysis = `Loss ${profitStr} tapi masih GRACE PERIOD (${ageText}/15m). Early cut TIDAK aktif — memberi waktu 1 candle M15 untuk develop. Hard SL tetap jadi safety net.`; + } else { + analysis = `Loss ${profitStr} (${ageText}), momentum: ${momentumVal.toFixed(0)}. ${momentumVal < -50 ? "Momentum lemah — early cut bisa trigger kapan saja!" : "Momentum belum terlalu buruk, masih ada harapan recovery."}${reversalWarns > 0 ? ` Reversal warning: ${reversalWarns}x.` : ""}`; + } + + insights.push({ + icon: pos.profit >= 5 ? + : pos.profit >= 0 ? + : pos.profit > -15 ? + : , + text: `📊 #${pos.ticket} ${dir} @ ${pos.priceOpen.toFixed(2)} — ${analysis}`, + type: pos.profit >= 5 ? "success" : pos.profit >= 0 ? "info" : pos.profit > -15 ? "warning" : "danger", + }); + } + } + + // ═══════════════════════════════════════════ + // 7. KENAPA TIDAK ENTRY (detail per filter) + // ═══════════════════════════════════════════ + if (!hasPositions) { + const filters = data.entryFilters || []; + const blockers = filters.filter((f) => !f.passed && !f.detail?.includes("[DISABLED]")); + const disabledFilters = filters.filter((f) => f.detail?.includes("[DISABLED]")); + const passedCount = filters.filter((f) => f.passed).length; + + if (blockers.length > 0) { + insights.push({ + icon: , + text: `Entry diblokir oleh ${blockers.length} filter (${passedCount}/${filters.length} lolos). Filter pertama yang gagal: "${blockers[0].name}" — ${blockers[0].detail || "tidak memenuhi syarat"}. Bot tidak akan entry sampai SEMUA filter hijau.`, + type: "warning", + }); + } else if (filters.length > 0 && blockers.length === 0) { + insights.push({ + icon: , + text: `Semua ${filters.length} filter terpenuhi! Bot siap entry begitu ada sinyal valid dari SMC + ML.`, + type: "success", + }); + } + + if (disabledFilters.length > 0) { + const names = disabledFilters.map((f) => f.name).join(", "); + insights.push({ + icon: , + text: `${disabledFilters.length} filter dinonaktifkan manual: ${names}. Filter ini di-bypass (auto-pass). Aktifkan kembali di panel Filters jika diperlukan.`, + type: "warning", + }); + } + } + + // ═══════════════════════════════════════════ + // 8. RISK & MODAL + // ═══════════════════════════════════════════ + const netPnl = (data.dailyProfit || 0) - (data.dailyLoss || 0); + if (data.dailyLoss > 0 || data.dailyProfit > 0) { + const pnlStr = netPnl >= 0 ? `+$${netPnl.toFixed(2)}` : `-$${Math.abs(netPnl).toFixed(2)}`; + const remaining = data.riskMode?.remainingDailyRisk; + const riskDetail = remaining !== undefined ? ` Sisa risk harian: $${remaining.toFixed(0)}.` : ""; + + if (netPnl < 0 && remaining !== undefined && remaining < 50) { + insights.push({ + icon: , + text: `⚠️ P/L hari ini: ${pnlStr} (loss $${data.dailyLoss.toFixed(2)}, profit $${data.dailyProfit.toFixed(2)}).${riskDetail} Mendekati batas — bot sangat konservatif.`, + type: "danger", + }); + } else { + insights.push({ + icon: netPnl >= 0 ? : , + text: `P/L hari ini: ${pnlStr} (loss $${data.dailyLoss.toFixed(2)}, profit $${data.dailyProfit.toFixed(2)}).${riskDetail}`, + type: netPnl >= 0 ? "success" : "warning", + }); + } + } + + if (data.riskMode) { + const mode = data.riskMode.mode?.toUpperCase() || "NORMAL"; + if (mode !== "NORMAL") { + insights.push({ + icon: , + text: `Risk mode: ${mode} — ${data.riskMode.reason}. Lot: ${data.riskMode.recommendedLot} (max ${data.riskMode.maxAllowedLot}).`, + type: mode === "RECOVERY" || mode === "PROTECTIVE" ? "danger" : "warning", + }); + } + } + + // ═══════════════════════════════════════════ + // 9. PERFORMA BOT + // ═══════════════════════════════════════════ + if (data.performance) { + const p = data.performance; + const uptimeText = p.uptimeHours < 1 + ? `${(p.uptimeHours * 60).toFixed(0)} menit` + : `${p.uptimeHours.toFixed(1)} jam`; + insights.push({ + icon: , + text: `Bot aktif ${uptimeText}, loop ke-${p.loopCount}. Avg execution: ${p.avgExecutionMs.toFixed(0)}ms. Session trades: ${p.totalSessionTrades} (P/L: ${p.totalSessionProfit >= 0 ? "+" : ""}$${p.totalSessionProfit.toFixed(2)}).`, + type: "info", + }); + } + + return insights; +} + +interface AssistantCardProps { + data: TradingStatus; +} + +export function AssistantCard({ data }: AssistantCardProps) { + const insights = generateInsights(data); + const [wibTime, setWibTime] = useState(""); + + useEffect(() => { + const update = () => { + setWibTime( + new Date().toLocaleString("id-ID", { + timeZone: "Asia/Jakarta", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + ); + }; + update(); + const interval = setInterval(update, 1000); + return () => clearInterval(interval); + }, []); + + return ( + + + + + Asisten Bot + + {wibTime} WIB + + + + +
+ {insights.map((insight, idx) => ( +
+ + {insight.icon} + + {insight.text} +
+ ))} +
+ + {/* Last analysis timestamp */} +
+ + Analisis terakhir: {data.timestamp + ? new Date(data.timestamp).toLocaleString("id-ID", { + timeZone: "Asia/Jakarta", + day: "2-digit", + month: "short", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + : wibTime + } WIB +
+
+
+ ); +} diff --git a/web-dashboard/src/components/dashboard/entry-filter-card.tsx b/web-dashboard/src/components/dashboard/entry-filter-card.tsx index 8dc5ed9..fade75c 100644 --- a/web-dashboard/src/components/dashboard/entry-filter-card.tsx +++ b/web-dashboard/src/components/dashboard/entry-filter-card.tsx @@ -2,16 +2,35 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Filter, Check, X, Minus } from "lucide-react"; import { cn } from "@/lib/utils"; +import { useFilterConfig } from "@/hooks/use-filter-config"; import type { EntryFilter } from "@/types/trading"; +/* Map bot filter display name → filter_config.json key */ +const NAME_TO_KEY: Record = { + "Flash Crash Guard": "flash_crash_guard", + "Regime Filter": "regime_filter", + "Risk Check": "risk_check", + "Session Filter": "session_filter", + "Spread Check": "spread_check", + "H1 Bias (#31B)": "h1_bias", + "ML Confidence": "ml_confidence", + "Signal Combination": "signal_combination", + "Time Filter (#34A)": "time_filter", + "Cooldown": "cooldown", + "Existing Position": "existing_position", +}; + interface EntryFilterCardProps { filters: EntryFilter[]; } export function EntryFilterCard({ filters }: EntryFilterCardProps) { + const { config, updating, toggleFilter } = useFilterConfig(); + const passedCount = filters.filter((f) => f.passed).length; const totalCount = filters.length; const hasBlocker = filters.some((f) => !f.passed); @@ -44,33 +63,66 @@ export function EntryFilterCard({ filters }: EntryFilterCardProps) { {filters.map((filter, idx) => { const isNotEvaluated = firstBlockerIdx >= 0 && idx > firstBlockerIdx; const isBlocker = !filter.passed && idx === firstBlockerIdx; + const isDisabled = filter.detail?.includes("[DISABLED]"); + + /* Resolve filter config key for toggle */ + const configKey = NAME_TO_KEY[filter.name]; + const filterEnabled = configKey && config + ? config.filters[configKey]?.enabled ?? true + : true; return ( - - -
- {isNotEvaluated ? ( - - ) : filter.passed ? ( - - ) : ( - - )} - +
+ {isDisabled ? ( + + ) : isNotEvaluated ? ( + + ) : filter.passed ? ( + + ) : ( + + )} + + + + {filter.name} -
- - -

{filter.detail || (filter.passed ? "Passed" : "Blocked")}

-
- + + +

+ {filter.detail || (filter.passed ? "Passed" : "Blocked")} + {configKey && !filterEnabled && " — Filter disabled, auto-pass"} +

+
+ + + {/* Toggle switch — outside tooltip so clicks work */} + {configKey && config && ( + toggleFilter(configKey)} + disabled={updating} + className="shrink-0 scale-75 origin-right" + /> + )} +
); })} diff --git a/web-dashboard/src/components/dashboard/filters-config-card.tsx b/web-dashboard/src/components/dashboard/filters-config-card.tsx deleted file mode 100644 index 16c3c10..0000000 --- a/web-dashboard/src/components/dashboard/filters-config-card.tsx +++ /dev/null @@ -1,128 +0,0 @@ -"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 3b815a7..0aea214 100644 --- a/web-dashboard/src/components/dashboard/index.ts +++ b/web-dashboard/src/components/dashboard/index.ts @@ -16,4 +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"; +export { AssistantCard } from "./assistant-card"; diff --git a/web-dashboard/src/hooks/use-filter-config.ts b/web-dashboard/src/hooks/use-filter-config.ts index d21adc0..c3dda00 100644 --- a/web-dashboard/src/hooks/use-filter-config.ts +++ b/web-dashboard/src/hooks/use-filter-config.ts @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import type { FilterConfigData, FilterUpdate } from '@/types/filters'; -const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8001'; +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; export function useFilterConfig() { const [config, setConfig] = useState(null);