feat: wire chain_client_sdk gRPC client + HubRepository scaffold
Adds a real Ch∆In hub backend to the Flutter client as a sibling-path dependency on chain_client_sdk_dart, alongside the existing MockRepository (which stays the default until the flow.completed-to-Evaluation pipeline lands in Phase 0 week 1). Wiring HubSettings (lib/data/hub_endpoint.dart) — persists host / port / secure / token / useHub flags via SharedPreferences under the Studio-compatible hub.* keys. Distinct from the SDK's chain.HubEndpoint, which is a transport-level value type — HubSettings adapts in that direction. HubRepository (lib/data/hub_repository.dart) — implements EvaluationRepository on top of HubClient. connect() runs a 4-second healthy() probe; on failure returns a repository with isHealthy=false rather than throwing, so the UI can surface "hub-down" without an exception-handling round-trip. list() / byEli() return empty data in Phase 0 because no hub-side flow yet produces Evaluation objects in the lawheatmap-specific shape. main.dart — loads HubSettings on startup; if useHub=false starts with MockRepository (fast offline default), otherwise probes the hub and falls back to MockRepository on probe failure. No mid-session swap to keep the Phase 0 wiring simple; a settings save instructs the user to restart. HubStatusPage — read-only summary card at the top, editable endpoint form, "Verbindung testen" probe button, "Speichern" persist button. Live-flow runbook below describes the chain serve / install / run / approve sequence so a user can wire the real flow themselves once Phase 1 lands. flutter analyze: clean. flutter test: 2/2 passing. Smoke-launch on macOS: app boots, Dart VM service comes up, no runtime errors in mock mode. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
fa8acd41b8
commit
39733628b6
6 changed files with 509 additions and 31 deletions
|
|
@ -1,17 +1,112 @@
|
|||
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';
|
||||
|
||||
class HubStatusPage extends StatelessWidget {
|
||||
/// 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<HubStatusPage> createState() => _HubStatusPageState();
|
||||
}
|
||||
|
||||
class _HubStatusPageState extends State<HubStatusPage> {
|
||||
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<void> _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<void> _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(
|
||||
|
|
@ -20,7 +115,7 @@ class HubStatusPage extends StatelessWidget {
|
|||
Text('Ch∆In Hub', style: t.displaySmall),
|
||||
const SizedBox(height: LawHeatmapSpace.xs),
|
||||
Text(
|
||||
'Status der Verbindung zum lokalen Hub und des Audit-Logs.',
|
||||
'Status der Verbindung und Konfiguration des lokalen Hubs.',
|
||||
style: t.bodyLarge?.copyWith(color: LawHeatmapColors.mute),
|
||||
),
|
||||
const SizedBox(height: LawHeatmapSpace.lg),
|
||||
|
|
@ -29,10 +124,19 @@ class HubStatusPage extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_row('Modus', repository.mode.toUpperCase(), t),
|
||||
_row('Quelle', repository.hubLabel, t),
|
||||
_row('gRPC-Endpunkt', '— (Phase-0-Stub)', t),
|
||||
_row('Audit-DB', '~/.chain/chain.db (geplant)', t),
|
||||
_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),
|
||||
],
|
||||
),
|
||||
|
|
@ -42,21 +146,120 @@ class HubStatusPage extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Geplante Live-Anbindung', style: t.headlineSmall),
|
||||
Text('Verbindung konfigurieren',
|
||||
style: t.headlineSmall),
|
||||
const SizedBox(height: LawHeatmapSpace.sm),
|
||||
Text(
|
||||
'1. `chain serve` startet lokal auf Port 50051.\n'
|
||||
'2. Diese App liest `~/.chain/hub-auth-token` und '
|
||||
'verbindet via gRPC.\n'
|
||||
'3. Capability `text.akoma_normalize@^0` muss '
|
||||
'installiert sein (`chain install text.akoma_normalize`).\n'
|
||||
'4. Pilot-Flow `flows/durchstich-gewo-14.yaml` läuft '
|
||||
'gegen GewO §14.\n'
|
||||
'5. Jedes step.completed-Event landet im Audit-Log; '
|
||||
'die Repository-Implementierung tauscht von MockRepository '
|
||||
'auf HubRepository (gleiches Interface, kein UI-Wechsel).',
|
||||
'`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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -73,7 +276,7 @@ class HubStatusPage extends StatelessWidget {
|
|||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 160,
|
||||
width: 200,
|
||||
child: Text(label,
|
||||
style: t.labelLarge
|
||||
?.copyWith(color: LawHeatmapColors.mute)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue