import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/network/api_client.dart'; import '../../../core/network/signalr_service.dart'; import '../../../core/theme/app_theme.dart'; import '../bloc/bot_bloc.dart'; import '../bloc/bot_event.dart'; import '../bloc/bot_state.dart'; import '../repositories/bot_repository.dart'; import '../widgets/bot_kpi_header.dart'; import '../widgets/bot_position_card.dart'; import '../widgets/bot_settings_sheet.dart'; class BotControlPanelScreen extends StatelessWidget { final ApiClient apiClient; final SignalRService signalRService; const BotControlPanelScreen({ super.key, required this.apiClient, required this.signalRService, }); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => BotBloc( repository: BotRepository(apiClient: apiClient), signalRService: signalRService, )..add(const FetchBotDashboard()), child: const _BotControlPanelContent(), ); } } class _BotControlPanelContent extends StatelessWidget { const _BotControlPanelContent(); void _openSettings(BuildContext context, BotLoaded state) { final botBloc = context.read(); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) => BotSettingsSheet( currentStatus: state.status, onSave: (autoExec, maxPos, risk, minScore) { botBloc.add(UpdateBotConfigSettings( autoExecutionEnabled: autoExec, maxPositions: maxPos, riskPerTradePercent: risk, minCompositeScore: minScore, )); }, onPanicClose: () { botBloc.add(const TriggerBotPanicClose()); }, ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppTheme.darkBackground, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, title: const Text('FinlyticBot Control Panel', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), actions: [ BlocBuilder( builder: (context, state) { if (state is BotLoaded) { return IconButton( onPressed: () => _openSettings(context, state), icon: const Icon(Icons.settings, color: Colors.white70), tooltip: 'Bot Einstellungen & Kill-Switch', ); } return const SizedBox.shrink(); }, ), IconButton( onPressed: () => context.read().add(const FetchBotDashboard()), icon: const Icon(Icons.refresh, color: Colors.white70), tooltip: 'Neu laden', ), ], ), body: BlocConsumer( listener: (context, state) { if (state is BotLoaded && state.actionMessage != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.actionMessage!), // A failed or partially-successful action (e.g. a panic-close that could not confirm every // position was closed) must never be shown in the same "all good" green as a full success // (Rules.md §4). backgroundColor: state.actionIsWarning ? AppTheme.accentRed : AppTheme.primaryEmerald, behavior: SnackBarBehavior.floating, ), ); } }, builder: (context, state) { if (state is BotLoading) { return Center(child: CircularProgressIndicator(color: AppTheme.primaryEmerald)); } if (state is BotError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: AppTheme.accentRed), const SizedBox(height: 12), Text(state.message, style: TextStyle(color: AppTheme.textMuted)), const SizedBox(height: 16), ElevatedButton( onPressed: () => context.read().add(const FetchBotDashboard()), style: ElevatedButton.styleFrom(backgroundColor: AppTheme.primaryEmerald), child: const Text('Erneut Versuchen'), ), ], ), ); } if (state is BotLoaded) { final activePositions = state.positions.where((p) => p.isActive).toList(); final closedPositions = state.positions.where((p) => p.isClosed).toList(); return RefreshIndicator( onRefresh: () async { context.read().add(const FetchBotDashboard()); }, color: AppTheme.primaryEmerald, backgroundColor: AppTheme.cardSurface, child: CustomScrollView( slivers: [ SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), sliver: SliverList( delegate: SliverChildListDelegate([ BotKpiHeader( summary: state.summary, status: state.status, totalUnrealizedPnL: state.totalUnrealizedPnL, totalRealizedPnL: state.totalRealizedPnL, ), const SizedBox(height: 20), _buildAlphaDecayMonitor(state), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Aktive Positionen (${activePositions.length}/${state.status.maxPositions})', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: AppTheme.primaryEmerald.withValues(alpha: 0.15), ), child: Text( 'Risk: ${(activePositions.length * state.status.riskPerTradePercent).toStringAsFixed(1)}%', style: TextStyle(color: AppTheme.primaryEmerald, fontSize: 11, fontWeight: FontWeight.bold), ), ), ], ), const SizedBox(height: 12), ]), ), ), if (activePositions.isEmpty) SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), sliver: SliverToBoxAdapter( child: Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: AppTheme.cardSurface.withValues(alpha: 0.5), border: Border.all(color: Colors.white10), ), child: Column( children: [ Icon(Icons.radar, size: 48, color: AppTheme.textMuted), const SizedBox(height: 12), const Text( 'Keine aktiven Positionen', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: 4), Text( 'Der Bot scannt das Universum nach Setup-Konfluenzen ab Score ≥ ${state.status.minCompositeScore}.', textAlign: TextAlign.center, style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), ], ), ), ), ) else SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), sliver: SliverList.builder( itemCount: activePositions.length, itemBuilder: (context, index) { final position = activePositions[index]; return BotPositionCard( position: position, onClosePressed: () { // Manual emergency close of this specific position }, ); }, ), ), if (closedPositions.isNotEmpty) ...[ SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), sliver: SliverToBoxAdapter( child: const Text( 'Kürzlich Geschlossene Trades', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.white70), ), ), ), SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16), sliver: SliverList.builder( itemCount: closedPositions.length > 5 ? 5 : closedPositions.length, itemBuilder: (context, index) { final position = closedPositions[index]; return Opacity( opacity: 0.7, child: BotPositionCard(position: position), ); }, ), ), ], const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), ); } return const SizedBox.shrink(); }, ), ); } // NOTE: This used to be an "Alpha-Decay & Strategy Reliability Monitor" // comparing hardcoded fake "Live vs Simulation" win rates / profit factors // per strategy (Rules.md §4 violation). There is no real data source for // that comparison: `GET /api/v1/simulation/matrix/{isin}` // (`StrategyAssetReliabilityDto`) only provides a simulated reliability // score per (ISIN, StrategyKey) pair — it has no "live" counterpart, and // this screen isn't scoped to a single asset, so there's no ISIN to query // it with in the first place. Rather than inventing numbers, or bolting on // an asset picker that isn't part of this task, this is now an explicit // empty state until a real live-vs-simulation data source exists. Widget _buildAlphaDecayMonitor(BotLoaded state) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: AppTheme.cardSurface, border: Border.all(color: Colors.white10), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.analytics_outlined, size: 16, color: Colors.cyanAccent), SizedBox(width: 6), Text('Alpha-Decay & Strategy Reliability Monitor', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.white)), ], ), const SizedBox(height: 12), Row( children: [ Icon(Icons.info_outline, size: 14, color: AppTheme.textMuted), const SizedBox(width: 8), Expanded( child: Text( 'Keine Live-vs-Simulation-Daten verfügbar.', style: TextStyle(color: AppTheme.textMuted, fontSize: 12), ), ), ], ), ], ), ); } }