Files
Finlytic/FinlyticApp/lib/features/admin/widgets/pipeline_settings_widget.dart
T

417 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import '../../../core/network/api_client.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/glass_container.dart';
import '../../../core/widgets/status_badge.dart';
/// Service Metadata Info used for Admin Config Navigation
class ServiceConfigMeta {
final String key;
final String displayName;
final String description;
final IconData icon;
final Color accentColor;
const ServiceConfigMeta({
required this.key,
required this.displayName,
required this.description,
required this.icon,
required this.accentColor,
});
}
/// Centralized Service Configuration Management Widget for Admin Panel.
class PipelineSettingsWidget extends StatefulWidget {
final ApiClient? apiClient;
const PipelineSettingsWidget({super.key, this.apiClient});
@override
State<PipelineSettingsWidget> createState() => _PipelineSettingsWidgetState();
}
class _PipelineSettingsWidgetState extends State<PipelineSettingsWidget> {
String _selectedServiceKey = 'FinlyticAssets';
bool _isLoading = false;
bool _isSaving = false;
static const List<ServiceConfigMeta> _services = [
ServiceConfigMeta(
key: 'FinlyticAssets',
displayName: 'Asset Katalog & Logos',
description: 'Verwaltet ISIN Asset-Stammdaten & Trade Republic Logo Fetcher',
icon: Icons.inventory_2_outlined,
accentColor: Color(0xFF00E5FF),
),
ServiceConfigMeta(
key: 'FinlyticNews',
displayName: 'News Scraper & AI',
description: 'RSS Web Scraper Intervall & Entwurf-Retention',
icon: Icons.newspaper_outlined,
accentColor: Color(0xFF3B82F6),
),
ServiceConfigMeta(
key: 'FinlyticTechnicalAnalysis',
displayName: 'Technische Analyse',
description: 'EMA/SMA Perioden, RSI Grenzwerte & Supertrend Multiplikator',
icon: Icons.show_chart_outlined,
accentColor: Color(0xFF8B5CF6),
),
ServiceConfigMeta(
key: 'FinlyticSentiment',
displayName: 'Sentiment NLP',
description: 'NLP Vertrauens-Schwellenwerte & Text-Batching',
icon: Icons.psychology_outlined,
accentColor: Color(0xFFEC4899),
),
ServiceConfigMeta(
key: 'FinlyticAnalyzer',
displayName: 'Analyzer Signal Engine',
description: 'Scraper Cron-Schedule & Minimaler Signal-Score',
icon: Icons.analytics_outlined,
accentColor: Color(0xFFF59E0B),
),
ServiceConfigMeta(
key: 'FinlyticTrades',
displayName: 'Trade Manager',
description: 'ATR Stop-Loss Multiplikator, Risiko-Prozente & Positionen',
icon: Icons.candlestick_chart_outlined,
accentColor: Color(0xFF10B981),
),
ServiceConfigMeta(
key: 'FinlyticFundamentals',
displayName: 'Fundamentaldaten',
description: 'Cache TTL Dauer & Yahoo Finance Fallback',
icon: Icons.corporate_fare_outlined,
accentColor: Color(0xFF06B6D4),
),
];
final Map<String, Map<String, TextEditingController>> _controllers = {
'FinlyticAssets': {
'TradeRepublicMaxRequestPageSize': TextEditingController(text: '100'),
'AssetUpdateTypeDelay': TextEditingController(text: '0'),
'BatchAssetUpdateDelay': TextEditingController(text: '5'),
},
'FinlyticNews': {
'ScrapingIntervalMinutes': TextEditingController(text: '15'),
'PollingFrequencyMinutes': TextEditingController(text: '15'),
'ArticleRetentionDays': TextEditingController(text: '90'),
'DefaultPageSize': TextEditingController(text: '20'),
},
'FinlyticTechnicalAnalysis': {
'EmaShortPeriod': TextEditingController(text: '20'),
'SmaMediumPeriod': TextEditingController(text: '50'),
'SmaLongPeriod': TextEditingController(text: '200'),
'RsiOverboughtLimit': TextEditingController(text: '70'),
'RsiOversoldLimit': TextEditingController(text: '30'),
'SupertrendMultiplier': TextEditingController(text: '3.0'),
},
'FinlyticSentiment': {
'MinConfidenceScore': TextEditingController(text: '0.70'),
'MaxBatchSize': TextEditingController(text: '50'),
},
'FinlyticAnalyzer': {
'ScanCronSchedule': TextEditingController(text: '0 */1 * * *'),
'MinSignalScore': TextEditingController(text: '75'),
'EnableLog_MqttHealthPing': TextEditingController(text: 'false'),
'EnableLog_MqttGeneral': TextEditingController(text: 'true'),
'EnableLog_AnalyzerAuto': TextEditingController(text: 'true'),
'EnableLog_AnalyzerManual': TextEditingController(text: 'true'),
'EnableLog_DatabaseOps': TextEditingController(text: 'true'),
},
'FinlyticTrades': {
'AtrStopLossMultiplier': TextEditingController(text: '1.5'),
'RiskPerTradePercentage': TextEditingController(text: '1.0'),
'MaxOpenPositions': TextEditingController(text: '5'),
},
'FinlyticFundamentals': {
'CacheTtlHours': TextEditingController(text: '24'),
'EnableYahooFallback': TextEditingController(text: 'true'),
},
};
@override
void initState() {
super.initState();
_fetchSettings();
}
Future<void> _fetchSettings() async {
if (widget.apiClient == null) return;
setState(() => _isLoading = true);
try {
final res = await widget.apiClient!.get('/api/v1/admin/settings');
if (res.statusCode == 200 && res.data is Map) {
final Map<String, dynamic> data = Map<String, dynamic>.from(res.data);
data.forEach((svc, items) {
if (items is List) {
_controllers.putIfAbsent(svc, () => {});
for (var item in items) {
final key = item['key']?.toString();
final val = item['value']?.toString();
if (key != null && val != null) {
if (_controllers[svc]!.containsKey(key)) {
_controllers[svc]![key]!.text = val;
} else {
_controllers[svc]![key] = TextEditingController(text: val);
}
}
}
}
});
}
} catch (_) {
// Retain standard default in-memory values
} finally {
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _saveSettings() async {
setState(() => _isSaving = true);
try {
final currentSvcControllers = _controllers[_selectedServiceKey] ?? {};
final payload = <String, String>{};
currentSvcControllers.forEach((k, v) {
payload[k] = v.text;
});
if (widget.apiClient != null) {
await widget.apiClient!.put('/api/v1/admin/settings/$_selectedServiceKey', data: payload);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle_outline, color: Colors.black),
const SizedBox(width: 8),
Expanded(
child: Text(
'Einstellungen für $_selectedServiceKey gespeichert & via MQTT synchronisiert.',
style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w600),
),
),
],
),
backgroundColor: AppTheme.primaryEmerald,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
);
}
} catch (ex) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Fehler beim Speichern: $ex'),
backgroundColor: AppTheme.accentRed,
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) setState(() => _isSaving = false);
}
}
@override
Widget build(BuildContext context) {
final activeService = _services.firstWhere((s) => s.key == _selectedServiceKey, orElse: () => _services.first);
final activeControllers = _controllers[_selectedServiceKey] ?? {};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Service Selection Ribbon
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _services.map((svc) {
final isSelected = svc.key == _selectedServiceKey;
return Padding(
padding: const EdgeInsets.only(right: 8, bottom: 12),
child: InkWell(
onTap: () => setState(() => _selectedServiceKey = svc.key),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: isSelected ? svc.accentColor.withValues(alpha: 0.15) : AppTheme.glassSurface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? svc.accentColor : AppTheme.glassBorder,
width: isSelected ? 1.5 : 1,
),
),
child: Row(
children: [
Icon(svc.icon, size: 18, color: isSelected ? svc.accentColor : AppTheme.textMuted),
const SizedBox(width: 8),
Text(
svc.displayName,
style: TextStyle(
fontSize: 13,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? AppTheme.textPrimary : AppTheme.textSecondary,
),
),
],
),
),
),
);
}).toList(),
),
),
const SizedBox(height: 8),
// Service Details & Config Panel
GlassContainer(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: activeService.accentColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: activeService.accentColor.withValues(alpha: 0.3)),
),
child: Icon(activeService.icon, color: activeService.accentColor, size: 22),
),
const SizedBox(width: 14),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
activeService.displayName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
Text(
activeService.description,
style: TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
],
),
],
),
if (_isLoading)
SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: AppTheme.primaryEmerald))
else
StatusBadge(label: 'MQTT Sync', color: AppTheme.primaryEmerald),
],
),
const SizedBox(height: 16),
const Divider(color: Colors.white10),
const SizedBox(height: 16),
// Parameter Input List
...activeControllers.entries.map((entry) {
final keyName = entry.key;
final controller = entry.value;
final isBoolean = controller.text.toLowerCase() == 'true' || controller.text.toLowerCase() == 'false';
if (isBoolean) {
final boolVal = controller.text.toLowerCase() == 'true';
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppTheme.glassSurface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppTheme.glassBorder),
),
child: SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(_formatLabel(keyName), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
//subtitle: Text('Schlüssel: $keyName', style: TextStyle(fontSize: 11, color: AppTheme.textMuted)),
value: boolVal,
activeThumbColor: activeService.accentColor,
onChanged: (val) => setState(() => controller.text = val.toString()),
),
);
}
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(
children: [
Expanded(
child: TextField(
controller: controller,
decoration: InputDecoration(
labelText: _formatLabel(keyName),
//helperText: 'Schlüssel: $keyName',
prefixIcon: Icon(Icons.tune_outlined, size: 18, color: activeService.accentColor),
),
),
),
/*if (isNumeric) ...[
const SizedBox(width: 8),
IconButton.filledTonal(
icon: const Icon(Icons.remove, size: 18),
onPressed: () => setState(() => _adjustNumericValue(controller, -1.0, isDouble: isDouble)),
),
IconButton.filledTonal(
icon: const Icon(Icons.add, size: 18),
onPressed: () => setState(() => _adjustNumericValue(controller, 1.0, isDouble: isDouble)),
),
],*/
],
),
);
}),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isSaving ? null : _saveSettings,
icon: _isSaving
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black))
: const Icon(Icons.save_outlined),
label: Text(
_isSaving ? 'Speichere & Sende via MQTT...' : 'Einstellungen für ${activeService.displayName} Speichern',
style: const TextStyle(fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryEmerald,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
),
],
),
),
],
);
}
String _formatLabel(String key) {
return key
.replaceAll(RegExp(r'(?<!^)(?=[A-Z])'), ' ')
.replaceAll('Minutes', '(Minuten)')
.replaceAll('Seconds', '(Sekunden)')
.replaceAll('Hours', '(Stunden)')
.replaceAll('Days', '(Tage)')
.replaceAll('Limit', 'Grenzwert')
.replaceAll('Period', 'Periode')
.replaceAll('Percentage', '(%)')
.replaceAll('Multiplier', 'Multiplikator');
}
}