import 'package:flutter/material.dart'; import '../data/hub_endpoint.dart'; import '../data/hub_repository.dart'; import '../data/repository.dart'; import '../theme/lawheatmap_tokens.dart'; import '../widgets/lawheatmap_card.dart'; /// Hub connection + settings. Read-only summary at the top, then /// an editable form for endpoint host/port/secure/token, then a /// "Verbinden / Test"-button. Changes are persisted via /// HubSettings and take effect on the next app start (Phase 0 /// keeps the running repository to avoid mid-session swap /// headaches). class HubStatusPage extends StatefulWidget { const HubStatusPage({super.key, required this.repository}); final EvaluationRepository repository; @override State createState() => _HubStatusPageState(); } class _HubStatusPageState extends State { HubSettings? _settings; late final TextEditingController _hostCtl; late final TextEditingController _portCtl; late final TextEditingController _tokenCtl; bool _secure = false; bool _useHub = false; String? _probeMessage; bool _probing = false; @override void initState() { super.initState(); _hostCtl = TextEditingController(); _portCtl = TextEditingController(); _tokenCtl = TextEditingController(); HubSettings.load().then((s) { setState(() { _settings = s; _hostCtl.text = s.host; _portCtl.text = s.port.toString(); _tokenCtl.text = s.authToken ?? ''; _secure = s.secure; _useHub = s.useHub; }); }); } @override void dispose() { _hostCtl.dispose(); _portCtl.dispose(); _tokenCtl.dispose(); super.dispose(); } HubSettings _formSettings() => HubSettings( host: _hostCtl.text.trim().isEmpty ? HubSettings.defaultHost : _hostCtl.text.trim(), port: int.tryParse(_portCtl.text.trim()) ?? HubSettings.defaultPort, secure: _secure, authToken: _tokenCtl.text.trim().isEmpty ? null : _tokenCtl.text.trim(), useHub: _useHub, ); Future _save() async { final s = _formSettings(); await s.save(); if (!mounted) return; setState(() => _settings = s); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Einstellungen gespeichert. ' 'Beim nächsten Start wird die App den Hub verwenden, ' 'wenn der Schalter „Hub nutzen" aktiv ist.'), ), ); } Future _probe() async { setState(() { _probing = true; _probeMessage = null; }); try { final hub = await HubRepository.connect(_formSettings()); setState(() { _probeMessage = hub.isHealthy ? 'Verbindung erfolgreich (healthy=true).' : 'Hub nicht erreichbar oder antwortet nicht SERVING.'; }); } catch (e) { setState(() => _probeMessage = 'Fehler: $e'); } finally { if (mounted) setState(() => _probing = false); } } @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; final repo = widget.repository; final s = _settings; return SingleChildScrollView( padding: const EdgeInsets.all(LawHeatmapSpace.xl), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Ch∆In Hub', style: t.displaySmall), const SizedBox(height: LawHeatmapSpace.xs), Text( 'Status der Verbindung und Konfiguration des lokalen Hubs.', style: t.bodyLarge?.copyWith(color: LawHeatmapColors.mute), ), const SizedBox(height: LawHeatmapSpace.lg), LawHeatmapCard( accent: true, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _row('Aktiver Modus', repo.mode.toUpperCase(), t), _row('Datenquelle', repo.hubLabel, t), _row( 'gRPC-Endpunkt', s == null ? '…' : s.url, t, ), _row( 'Hub-Nutzung beim Start', s == null ? '…' : (s.useHub ? 'ja' : 'nein'), t, ), _row('Audit-DB', '~/.chain/chain.db (Hub-seitig)', t), _row('Hash-Tag', 'CHAIN-EVENT-V3', t), ], ), ), const SizedBox(height: LawHeatmapSpace.lg), LawHeatmapCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Verbindung konfigurieren', style: t.headlineSmall), const SizedBox(height: LawHeatmapSpace.sm), Text( '`chain serve` läuft standardmäßig auf ' '127.0.0.1:50051 ohne Auth. ' 'Bei aktiver Auth den Bearer-Token aus ' '~/.chain/hub-auth-token hier eintragen.', style: t.labelSmall ?.copyWith(color: LawHeatmapColors.mute), ), const SizedBox(height: LawHeatmapSpace.lg), Row(children: [ Expanded( flex: 3, child: TextField( controller: _hostCtl, decoration: const InputDecoration( labelText: 'Host', hintText: '127.0.0.1', ), ), ), const SizedBox(width: LawHeatmapSpace.md), Expanded( child: TextField( controller: _portCtl, decoration: const InputDecoration( labelText: 'Port', hintText: '50051', ), keyboardType: TextInputType.number, ), ), ]), const SizedBox(height: LawHeatmapSpace.md), TextField( controller: _tokenCtl, decoration: const InputDecoration( labelText: 'Auth-Token (optional)', ), obscureText: true, ), const SizedBox(height: LawHeatmapSpace.md), Row(children: [ Switch( value: _secure, onChanged: (v) => setState(() => _secure = v), ), const SizedBox(width: LawHeatmapSpace.sm), const Text('TLS (https)'), const SizedBox(width: LawHeatmapSpace.xl), Switch( value: _useHub, onChanged: (v) => setState(() => _useHub = v), ), const SizedBox(width: LawHeatmapSpace.sm), const Text('Hub beim Start nutzen'), ]), const SizedBox(height: LawHeatmapSpace.lg), Row(children: [ FilledButton.icon( onPressed: _probing ? null : _probe, icon: _probing ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Icon(Icons.bolt_outlined), label: const Text('Verbindung testen'), ), const SizedBox(width: LawHeatmapSpace.md), OutlinedButton.icon( onPressed: _save, icon: const Icon(Icons.save_outlined), label: const Text('Speichern'), ), ]), if (_probeMessage != null) ...[ const SizedBox(height: LawHeatmapSpace.md), Text(_probeMessage!, style: t.labelLarge?.copyWith( color: LawHeatmapColors.signal, )), ], ], ), ), const SizedBox(height: LawHeatmapSpace.lg), LawHeatmapCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Geplanter Live-Flow', style: t.headlineSmall), const SizedBox(height: LawHeatmapSpace.sm), Text( '1. `chain serve` auf localhost:50051 starten.\n' '2. Module installieren (`chain install`).\n' '3. Flow ausführen:\n' ' chain run flows/durchstich-gewo-14.yaml \\\n' ' --input norm_url=https://www.gesetze-im-internet.de/gewo/__14.xml \\\n' ' --input cohort_id=berlin-kmu\n' '4. `chain admin approvals list` → ' 'Juristen-Gate freigeben.\n' '5. App neu starten — Hub-Modus aktiv, ' 'flow.completed-Events werden in ' 'Evaluation-Objekte übersetzt.', style: t.bodyLarge?.copyWith( height: 1.6, fontFamily: LawHeatmapTypography.mono, fontSize: 12, ), ), ], ), ), ], ), ); } Widget _row(String label, String value, TextTheme t) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ SizedBox( width: 200, child: Text(label, style: t.labelLarge ?.copyWith(color: LawHeatmapColors.mute)), ), Expanded( child: Text(value, style: t.bodyLarge ?.copyWith(fontFamily: LawHeatmapTypography.mono)), ), ], ), ); } }