diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 049af98..ad82cf9 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -561,13 +561,6 @@ "auditFilterModule": "Modul", "auditFilterTooltip": "Ereignisse filtern", "auditClearLogTooltip": "Protokoll löschen (nur local/dev Kanal)", - "auditMoreTooltip": "Weitere Aktionen", - "auditSearchHint": "Ereignisse durchsuchen (Flow, Schritt, Modul, Fehlertext …)", - "auditExportAction": "Aktuelle Ansicht als JSONL exportieren …", - "auditExportNothing": "Keine Ereignisse in der aktuellen Ansicht — nichts zu exportieren.", - "auditExportSaved": "{n} Ereignisse exportiert nach {path}", - "@auditExportSaved": {"placeholders": {"n": {"type": "int"}, "path": {"type": "String"}}}, - "auditDevResetAction": "Entwicklungs-Reset: Protokoll löschen … (nur local/dev)", "auditClearedToast": "{n} Ereignisse auf Kanal „{channel}\" gelöscht. Kette mit Marker neu geseedet.", "@auditClearedToast": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a655ae0..55b1cf2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -579,13 +579,6 @@ "auditFilterModule": "module", "auditFilterTooltip": "Filter events", "auditClearLogTooltip": "Clear log (local/dev channels only)", - "auditMoreTooltip": "More actions", - "auditSearchHint": "Search events (flow, step, module, error text …)", - "auditExportAction": "Export current view as JSONL …", - "auditExportNothing": "No events in the current view — nothing to export.", - "auditExportSaved": "Exported {n} events to {path}", - "@auditExportSaved": {"placeholders": {"n": {"type": "int"}, "path": {"type": "String"}}}, - "auditDevResetAction": "Development reset: clear log … (local/dev only)", "auditClearedToast": "Cleared {n} events on channel \"{channel}\". Chain reseeded with marker.", "@auditClearedToast": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1c18855..a19dbb2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2126,42 +2126,6 @@ abstract class AppLocalizations { /// **'Clear log (local/dev channels only)'** String get auditClearLogTooltip; - /// No description provided for @auditMoreTooltip. - /// - /// In en, this message translates to: - /// **'More actions'** - String get auditMoreTooltip; - - /// No description provided for @auditSearchHint. - /// - /// In en, this message translates to: - /// **'Search events (flow, step, module, error text …)'** - String get auditSearchHint; - - /// No description provided for @auditExportAction. - /// - /// In en, this message translates to: - /// **'Export current view as JSONL …'** - String get auditExportAction; - - /// No description provided for @auditExportNothing. - /// - /// In en, this message translates to: - /// **'No events in the current view — nothing to export.'** - String get auditExportNothing; - - /// No description provided for @auditExportSaved. - /// - /// In en, this message translates to: - /// **'Exported {n} events to {path}'** - String auditExportSaved(int n, String path); - - /// No description provided for @auditDevResetAction. - /// - /// In en, this message translates to: - /// **'Development reset: clear log … (local/dev only)'** - String get auditDevResetAction; - /// No description provided for @auditClearedToast. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e286065..017c2a0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1181,29 +1181,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get auditClearLogTooltip => 'Protokoll löschen (nur local/dev Kanal)'; - @override - String get auditMoreTooltip => 'Weitere Aktionen'; - - @override - String get auditSearchHint => - 'Ereignisse durchsuchen (Flow, Schritt, Modul, Fehlertext …)'; - - @override - String get auditExportAction => 'Aktuelle Ansicht als JSONL exportieren …'; - - @override - String get auditExportNothing => - 'Keine Ereignisse in der aktuellen Ansicht — nichts zu exportieren.'; - - @override - String auditExportSaved(int n, String path) { - return '$n Ereignisse exportiert nach $path'; - } - - @override - String get auditDevResetAction => - 'Entwicklungs-Reset: Protokoll löschen … (nur local/dev)'; - @override String auditClearedToast(int n, String channel) { return '$n Ereignisse auf Kanal „$channel\" gelöscht. Kette mit Marker neu geseedet.'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6073599..89f3b27 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1198,29 +1198,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get auditClearLogTooltip => 'Clear log (local/dev channels only)'; - @override - String get auditMoreTooltip => 'More actions'; - - @override - String get auditSearchHint => - 'Search events (flow, step, module, error text …)'; - - @override - String get auditExportAction => 'Export current view as JSONL …'; - - @override - String get auditExportNothing => - 'No events in the current view — nothing to export.'; - - @override - String auditExportSaved(int n, String path) { - return 'Exported $n events to $path'; - } - - @override - String get auditDevResetAction => - 'Development reset: clear log … (local/dev only)'; - @override String auditClearedToast(int n, String channel) { return 'Cleared $n events on channel \"$channel\". Chain reseeded with marker.'; diff --git a/lib/pages/audit.dart b/lib/pages/audit.dart index ad7ebcf..5eb2a0a 100644 --- a/lib/pages/audit.dart +++ b/lib/pages/audit.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import '../data/error_presentation.dart'; @@ -14,21 +13,6 @@ import '../theme/tokens.dart'; import '../widgets/widgets.dart'; import 'welcome.dart' show showFaiDoc; -/// Case-insensitive free-text match over every field the audit list -/// displays or exports. [query] must already be lowercased/trimmed. -/// Top-level so the filter behaviour is unit-testable. -bool matchesAuditQuery(AuditEvent e, String query) { - bool has(String? s) => s != null && s.toLowerCase().contains(query); - return has(e.type) || - has(e.flowName) || - has(e.stepId) || - has(e.moduleName) || - has(e.error) || - has(e.detail) || - has(e.project) || - has(e.eventId); -} - class AuditPage extends StatefulWidget { const AuditPage({super.key}); @@ -38,7 +22,6 @@ class AuditPage extends StatefulWidget { class _AuditPageState extends State { String _typeFilter = 'all'; - String _search = ''; List _events = const []; String? _error; bool _initialLoaded = false; @@ -110,62 +93,6 @@ class _AuditPageState extends State { }); } - /// The list as the user currently sees it: type chip first, then - /// the free-text query over every displayed/exported field. - List _visibleEvents() { - final byType = _typeFilter == 'all' - ? _events - : _events.where((e) => e.type.startsWith(_typeFilter)).toList(); - final q = _search.trim().toLowerCase(); - if (q.isEmpty) return byType; - return byType.where((e) => matchesAuditQuery(e, q)).toList(); - } - - /// Export the currently visible (type- + search-filtered) events - /// as JSONL — one JSON object per line, newest first, exactly what - /// the list shows. For full-history exports with WORM guarantees - /// the CLI `chain audit export` stays the canonical tool. - Future _onExportPressed() async { - final l = AppLocalizations.of(context)!; - final events = _visibleEvents(); - if (events.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l.auditExportNothing)), - ); - return; - } - final path = await FilePicker.saveFile( - dialogTitle: l.auditExportAction, - fileName: 'audit-export.jsonl', - ); - if (path == null || !mounted) return; - try { - final lines = events.map((e) => jsonEncode({ - 'event_id': e.eventId, - 'timestamp': e.timestamp.toIso8601String(), - 'type': e.type, - if (e.project.isNotEmpty) 'project': e.project, - if (e.flowName != null) 'flow': e.flowName, - if (e.stepId != null) 'step': e.stepId, - if (e.moduleName != null) 'module': e.moduleName, - if (e.moduleVersion != null) 'module_version': e.moduleVersion, - if (e.invocationId != null) 'invocation_id': e.invocationId, - if (e.flowExecution != null) 'flow_execution': e.flowExecution, - if (e.durationMs != null) 'duration_ms': e.durationMs, - if (e.error != null) 'error': e.error, - if (e.detail != null) 'detail': e.detail, - })); - await File(path).writeAsString('${lines.join('\n')}\n', flush: true); - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l.auditExportSaved(events.length, path))), - ); - } catch (e) { - if (!mounted) return; - showChainErrorSnack(context, 'audit.export', e); - } - } - Future _onClearPressed() async { final l = AppLocalizations.of(context)!; final outcome = await _ClearAuditDialog.show(context); @@ -217,7 +144,9 @@ class _AuditPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final filtered = _visibleEvents(); + final filtered = _typeFilter == 'all' + ? _events + : _events.where((e) => e.type.startsWith(_typeFilter)).toList(); return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, @@ -246,40 +175,10 @@ class _AuditPageState extends State { tooltip: AppLocalizations.of(context)!.helpTooltip, onPressed: () => showFaiDoc(context, 'audit'), ), - // The dev-only reset used to sit here as a bare trash icon — - // on an audit log that read as "delete evidence" (usertest - // panel, security auditor). It now lives in a labeled - // overflow menu next to the export action. - PopupMenuButton( - icon: const Icon(Icons.more_vert, size: 18), - tooltip: AppLocalizations.of(context)!.auditMoreTooltip, - onSelected: (v) => switch (v) { - 'export' => _onExportPressed(), - 'clear' => _onClearPressed(), - _ => null, - }, - itemBuilder: (ctx) => [ - PopupMenuItem( - value: 'export', - child: Row( - children: [ - const Icon(Icons.download_outlined, size: 16), - const SizedBox(width: ChainSpace.sm), - Text(AppLocalizations.of(ctx)!.auditExportAction), - ], - ), - ), - PopupMenuItem( - value: 'clear', - child: Row( - children: [ - const Icon(Icons.delete_sweep_outlined, size: 16), - const SizedBox(width: ChainSpace.sm), - Text(AppLocalizations.of(ctx)!.auditDevResetAction), - ], - ), - ), - ], + IconButton( + icon: const Icon(Icons.delete_sweep_outlined, size: 18), + tooltip: AppLocalizations.of(context)!.auditClearLogTooltip, + onPressed: _onClearPressed, ), const SizedBox(width: ChainSpace.sm), ], @@ -287,26 +186,6 @@ class _AuditPageState extends State { body: Column( children: [ _LiveStatusBar(eventCount: filtered.length, error: _error), - Padding( - padding: const EdgeInsets.fromLTRB( - ChainSpace.xl, - ChainSpace.sm, - ChainSpace.xl, - 0, - ), - child: TextField( - onChanged: (v) => setState(() => _search = v), - decoration: InputDecoration( - hintText: AppLocalizations.of(context)!.auditSearchHint, - prefixIcon: const Icon(Icons.search, size: 16), - isDense: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - style: theme.textTheme.bodySmall, - ), - ), Expanded( child: !_initialLoaded ? const Center(child: CircularProgressIndicator()) diff --git a/lib/pages/federation.dart b/lib/pages/federation.dart index fa5210d..a19a25c 100644 --- a/lib/pages/federation.dart +++ b/lib/pages/federation.dart @@ -93,19 +93,6 @@ class _FederationPageState extends State { appBar: AppBar( title: Text(l.federationTitle), actions: [ - // Primary action lives in the AppBar like every other - // page (Flows: "New flow", Store: "Add store") — no FAB. - Padding( - padding: const EdgeInsets.only(right: ChainSpace.sm), - child: OutlinedButton.icon( - icon: const Icon(Icons.add_link, size: 16), - label: Text(l.federationAddSatellite), - onPressed: _addSatellite, - style: OutlinedButton.styleFrom( - visualDensity: VisualDensity.compact, - ), - ), - ), IconButton( icon: const Icon(Icons.help_outline, size: 18), tooltip: l.helpTooltip, @@ -119,6 +106,11 @@ class _FederationPageState extends State { const SizedBox(width: ChainSpace.sm), ], ), + floatingActionButton: FloatingActionButton.extended( + onPressed: _addSatellite, + icon: const Icon(Icons.add_link), + label: Text(l.federationAddSatellite), + ), body: FutureBuilder>( future: _future, builder: (context, snapshot) { @@ -139,14 +131,13 @@ class _FederationPageState extends State { } final sats = snapshot.data ?? []; if (sats.isEmpty) { + // The "add satellite" action lives in the FAB (always + // visible); don't repeat it here — that showed the button + // twice on the empty page. return ChainEmptyState( icon: Icons.hub_outlined, title: l.federationEmptyTitle, hint: l.federationEmptyHint, - action: FilledButton.tonal( - onPressed: _addSatellite, - child: Text(l.federationAddSatellite), - ), ); } return ListView.separated( diff --git a/test/audit_filter_test.dart b/test/audit_filter_test.dart deleted file mode 100644 index c76392b..0000000 --- a/test/audit_filter_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -// Free-text audit search — the match function behind the audit -// page's search field and JSONL export ("current view" semantics). - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:chain_studio/data/hub.dart'; -import 'package:chain_studio/pages/audit.dart'; - -AuditEvent _event({ - String type = 'step.completed', - String? flow, - String? step, - String? module, - String? error, - String? detail, - String project = '', -}) { - return AuditEvent( - eventId: 'evt-1', - timestamp: DateTime.utc(2026, 7, 18), - type: type, - flowName: flow, - stepId: step, - moduleName: module, - error: error, - detail: detail, - project: project, - ); -} - -void main() { - test('matches on every displayed field, case-insensitive', () { - expect(matchesAuditQuery(_event(flow: 'Rechnungslauf'), 'rechnung'), isTrue); - expect(matchesAuditQuery(_event(step: 'extract'), 'extract'), isTrue); - expect(matchesAuditQuery(_event(module: 'debug.echo'), 'echo'), isTrue); - expect(matchesAuditQuery(_event(error: 'Timeout nach 30s'), 'timeout'), - isTrue); - expect(matchesAuditQuery(_event(detail: '{"anchor":"tsa"}'), 'anchor'), - isTrue); - expect(matchesAuditQuery(_event(project: 'stromnetz'), 'strom'), isTrue); - expect(matchesAuditQuery(_event(), 'evt-1'), isTrue); - expect(matchesAuditQuery(_event(), 'step.'), isTrue); - }); - - test('rejects non-matching queries and null fields', () { - expect(matchesAuditQuery(_event(), 'nichtvorhanden'), isFalse); - expect(matchesAuditQuery(_event(flow: null, error: null), 'flow'), isFalse); - }); -}