From ae97b882573bd6f508186da13d36f59760878743 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Sat, 18 Jul 2026 00:20:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(runs):=20honest=20empty=20state=20=E2=80=94?= =?UTF-8?q?=20feature=20off=20vs.=20no=20runs=20yet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The empty state unconditionally claimed background runs were switched off, even on hubs where the operator had enabled them. The hub now reports the flag on the list RPC (detached_enabled); the hint picks the truthful variant. Signed-off-by: flemming-it --- lib/data/hub.dart | 17 ++++++++++------- lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_localizations.dart | 6 ++++++ lib/l10n/app_localizations_de.dart | 4 ++++ lib/l10n/app_localizations_en.dart | 4 ++++ lib/pages/runs.dart | 11 ++++++++--- 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/data/hub.dart b/lib/data/hub.dart index f51dc49..1939e6e 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -1136,16 +1136,19 @@ class HubService { Future reject(String id, String reviewer, String reason) => _client.reject(approvalId: id, reviewer: reviewer, reason: reason); - /// Every tracked detached invocation (newest-first). Optionally - /// scoped to one [project]. Empty when detached invocations are - /// disabled or none have run this process. - Future> listDetachedRuns({String project = ''}) async { - final entries = await _client.listInvocations(); - final runs = entries + /// Detached-runs monitor snapshot: every tracked invocation + /// (newest-first, optionally scoped to one [project]) plus whether + /// the operator enabled the feature at all — so the empty state + /// can say "switched off" vs. "on, but no runs yet" truthfully. + Future<({List runs, bool enabled})> listDetachedRuns({ + String project = '', + }) async { + final r = await _client.listInvocationsFull(); + final runs = r.invocations .where((e) => project.isEmpty || e.project == project) .map(DetachedRun.fromEntry) .toList(); - return runs; + return (runs: runs, enabled: r.detachedEnabled); } /// Cancel a running/pending detached invocation. Returns true when diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 049af98..cb4587a 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1744,6 +1744,7 @@ "runsReloadTooltip": "Lauf-Liste neu laden", "runsEmptyTitle": "Keine Läufe im Hintergrund", "runsEmptyHint": "Hier erscheinen Läufe, die im Hintergrund weiterlaufen, während Sie anderes tun. Diese Funktion ist optional und standardmäßig ausgeschaltet — die Anleitung zeigt Schritt für Schritt, wie sie eingeschaltet wird.", + "runsEmptyEnabledHint": "Läufe im Hintergrund sind eingeschaltet — es wurde nur noch keiner gestartet. Starten Sie einen Flow mit der Option „im Hintergrund ausführen“, dann erscheint er hier.", "runsEmptyGuideButton": "Anleitung öffnen", "runsCancelButton": "Abbrechen", "runsCancelSignalled": "Abbruch für {flow} ausgelöst.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a655ae0..9cd599b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1783,6 +1783,7 @@ "runsReloadTooltip": "Reload the runs list", "runsEmptyTitle": "No background runs", "runsEmptyHint": "Runs that keep working in the background while you do something else appear here. The feature is optional and off by default — the guide shows step by step how to turn it on.", + "runsEmptyEnabledHint": "Background runs are switched on — none has been started yet. Start a flow with the \"run in background\" option and it will appear here.", "runsEmptyGuideButton": "Open the guide", "runsCancelButton": "Cancel", "runsCancelSignalled": "Cancel signalled for {flow}.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1c18855..8e25421 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5299,6 +5299,12 @@ abstract class AppLocalizations { /// **'Runs that keep working in the background while you do something else appear here. The feature is optional and off by default — the guide shows step by step how to turn it on.'** String get runsEmptyHint; + /// No description provided for @runsEmptyEnabledHint. + /// + /// In en, this message translates to: + /// **'Background runs are switched on — none has been started yet. Start a flow with the \"run in background\" option and it will appear here.'** + String get runsEmptyEnabledHint; + /// No description provided for @runsEmptyGuideButton. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e286065..1933b5b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3135,6 +3135,10 @@ class AppLocalizationsDe extends AppLocalizations { String get runsEmptyHint => 'Hier erscheinen Läufe, die im Hintergrund weiterlaufen, während Sie anderes tun. Diese Funktion ist optional und standardmäßig ausgeschaltet — die Anleitung zeigt Schritt für Schritt, wie sie eingeschaltet wird.'; + @override + String get runsEmptyEnabledHint => + 'Läufe im Hintergrund sind eingeschaltet — es wurde nur noch keiner gestartet. Starten Sie einen Flow mit der Option „im Hintergrund ausführen“, dann erscheint er hier.'; + @override String get runsEmptyGuideButton => 'Anleitung öffnen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 6073599..781d566 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3136,6 +3136,10 @@ class AppLocalizationsEn extends AppLocalizations { String get runsEmptyHint => 'Runs that keep working in the background while you do something else appear here. The feature is optional and off by default — the guide shows step by step how to turn it on.'; + @override + String get runsEmptyEnabledHint => + 'Background runs are switched on — none has been started yet. Start a flow with the \"run in background\" option and it will appear here.'; + @override String get runsEmptyGuideButton => 'Open the guide'; diff --git a/lib/pages/runs.dart b/lib/pages/runs.dart index 89f4207..6f458c0 100644 --- a/lib/pages/runs.dart +++ b/lib/pages/runs.dart @@ -24,6 +24,7 @@ class RunsPage extends StatefulWidget { class _RunsPageState extends State { List _runs = const []; + bool _detachedEnabled = false; String? _error; bool _loaded = false; Timer? _poll; @@ -49,12 +50,13 @@ class _RunsPageState extends State { Future _refresh() async { try { - final runs = await HubService.instance.listDetachedRuns( + final snapshot = await HubService.instance.listDetachedRuns( project: Workspace.instance.activeSlug, ); if (!mounted) return; setState(() { - _runs = runs; + _runs = snapshot.runs; + _detachedEnabled = snapshot.enabled; _error = null; _loaded = true; }); @@ -126,7 +128,10 @@ class _RunsPageState extends State { ? ChainEmptyState( icon: Icons.rocket_launch_outlined, title: l.runsEmptyTitle, - hint: l.runsEmptyHint, + // The hub reports whether the operator enabled the + // feature — never claim "switched off" while it is on + // and there simply are no runs yet (usertest finding). + hint: _detachedEnabled ? l.runsEmptyEnabledHint : l.runsEmptyHint, // The guide carries the plain-language explanation plus // the exact operator steps (config snippet) — a click // target instead of a raw config key in the hint.