Recl∆Im — the F∆I product line for Pflicht-basierte
Wirkungsanalyse und Reform-Vorschlags-Pipelines. Replaces the
working title 'Law-Heatmap', which described the demo view
rather than the platform. Heatmap, Norms list, Hub view,
Methodik are now four panes of the same Recl∆Im app.
Renames in this commit:
Flutter package lawheatmap_app -> reclaim_app
macOS bundle id ai.flemming.lawheatmapApp -> ai.flemming.reclaim
macOS PRODUCT_NAME lawheatmap_app -> reclaim_app
Theme tokens LawHeatmapColors/Space/Radius/Typography/Theme
-> Reclaim*
Surface card LawHeatmapCard -> ReclaimCard
Top-level widget LawHeatmapApp -> ReclaimApp
Theme files lawheatmap_theme.dart/lawheatmap_tokens.dart
-> reclaim_theme.dart/reclaim_tokens.dart
Widget file lawheatmap_card.dart -> reclaim_card.dart
Build script build-macos.sh paths and headline string
Docs MACHBARKEITSSTUDIE.md, METHODIK.md, RUN.md,
app/README.md, flow YAML
UI strings 'F∆I Law-Heatmap' / 'Law-Heatmap'
-> 'Recl∆Im' / 'Recl∆Im (F∆I)' for vendor-
prefixed contexts
flutter analyze: clean. flutter test: 2/2 passing.
The local working directory stays fai_lawheatmap/ for now —
the dir is referenced by the .claude project metadata; renaming
it would break session continuity for no real gain. The Forgejo
repo was renamed via API in the same change-set (fai/lawheatmap
-> fai/reclaim), with the legacy slug serving a redirect
courtesy of Forgejo's built-in rename handling.
Signed-off-by: flemming-it <sf@flemming.it>
296 lines
10 KiB
Dart
296 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../data/hub_endpoint.dart';
|
|
import '../data/hub_repository.dart';
|
|
import '../data/repository.dart';
|
|
import '../theme/reclaim_tokens.dart';
|
|
import '../widgets/hub_capabilities_card.dart';
|
|
import '../widgets/reclaim_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<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(ReclaimSpace.xl),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Ch∆In Hub', style: t.displaySmall),
|
|
const SizedBox(height: ReclaimSpace.xs),
|
|
Text(
|
|
'Status der Verbindung und Konfiguration des lokalen Hubs.',
|
|
style: t.bodyLarge?.copyWith(color: ReclaimColors.mute),
|
|
),
|
|
const SizedBox(height: ReclaimSpace.lg),
|
|
ReclaimCard(
|
|
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: ReclaimSpace.lg),
|
|
ReclaimCard(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Verbindung konfigurieren',
|
|
style: t.headlineSmall),
|
|
const SizedBox(height: ReclaimSpace.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: ReclaimColors.mute),
|
|
),
|
|
const SizedBox(height: ReclaimSpace.lg),
|
|
Row(children: [
|
|
Expanded(
|
|
flex: 3,
|
|
child: TextField(
|
|
controller: _hostCtl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Host',
|
|
hintText: '127.0.0.1',
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: ReclaimSpace.md),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _portCtl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Port',
|
|
hintText: '50051',
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
),
|
|
]),
|
|
const SizedBox(height: ReclaimSpace.md),
|
|
TextField(
|
|
controller: _tokenCtl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Auth-Token (optional)',
|
|
),
|
|
obscureText: true,
|
|
),
|
|
const SizedBox(height: ReclaimSpace.md),
|
|
Row(children: [
|
|
Switch(
|
|
value: _secure,
|
|
onChanged: (v) => setState(() => _secure = v),
|
|
),
|
|
const SizedBox(width: ReclaimSpace.sm),
|
|
const Text('TLS (https)'),
|
|
const SizedBox(width: ReclaimSpace.xl),
|
|
Switch(
|
|
value: _useHub,
|
|
onChanged: (v) => setState(() => _useHub = v),
|
|
),
|
|
const SizedBox(width: ReclaimSpace.sm),
|
|
const Text('Hub beim Start nutzen'),
|
|
]),
|
|
const SizedBox(height: ReclaimSpace.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: ReclaimSpace.md),
|
|
OutlinedButton.icon(
|
|
onPressed: _save,
|
|
icon: const Icon(Icons.save_outlined),
|
|
label: const Text('Speichern'),
|
|
),
|
|
]),
|
|
if (_probeMessage != null) ...[
|
|
const SizedBox(height: ReclaimSpace.md),
|
|
Text(_probeMessage!,
|
|
style: t.labelLarge?.copyWith(
|
|
color: ReclaimColors.signal,
|
|
)),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: ReclaimSpace.lg),
|
|
HubCapabilitiesCard(repository: repo),
|
|
const SizedBox(height: ReclaimSpace.lg),
|
|
ReclaimCard(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Geplanter Live-Flow', style: t.headlineSmall),
|
|
const SizedBox(height: ReclaimSpace.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: ReclaimTypography.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: ReclaimColors.mute)),
|
|
),
|
|
Expanded(
|
|
child: Text(value,
|
|
style: t.bodyLarge
|
|
?.copyWith(fontFamily: ReclaimTypography.mono)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|