Compare commits
No commits in common. "5aa69104e70c8acb0eb03b70a5b54ae9d0201685" and "b52590c7d3d400daba1f0ac8cf41141a7118cf2a" have entirely different histories.
5aa69104e7
...
b52590c7d3
8 changed files with 15 additions and 290 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -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<AuditPage> {
|
||||
String _typeFilter = 'all';
|
||||
String _search = '';
|
||||
List<AuditEvent> _events = const [];
|
||||
String? _error;
|
||||
bool _initialLoaded = false;
|
||||
|
|
@ -110,62 +93,6 @@ class _AuditPageState extends State<AuditPage> {
|
|||
});
|
||||
}
|
||||
|
||||
/// The list as the user currently sees it: type chip first, then
|
||||
/// the free-text query over every displayed/exported field.
|
||||
List<AuditEvent> _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<void> _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<void> _onClearPressed() async {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final outcome = await _ClearAuditDialog.show(context);
|
||||
|
|
@ -217,7 +144,9 @@ class _AuditPageState extends State<AuditPage> {
|
|||
@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<AuditPage> {
|
|||
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<String>(
|
||||
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<AuditPage> {
|
|||
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())
|
||||
|
|
|
|||
|
|
@ -93,19 +93,6 @@ class _FederationPageState extends State<FederationPage> {
|
|||
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<FederationPage> {
|
|||
const SizedBox(width: ChainSpace.sm),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _addSatellite,
|
||||
icon: const Icon(Icons.add_link),
|
||||
label: Text(l.federationAddSatellite),
|
||||
),
|
||||
body: FutureBuilder<List<Satellite>>(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
|
|
@ -139,14 +131,13 @@ class _FederationPageState extends State<FederationPage> {
|
|||
}
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue