fix(runs): honest empty state — feature off vs. no runs yet
Some checks are pending
Security / Security check (push) Waiting to run

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 <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-18 00:20:43 +02:00
parent 5aa69104e7
commit ae97b88257
7 changed files with 34 additions and 10 deletions

View file

@ -1136,16 +1136,19 @@ class HubService {
Future<void> reject(String id, String reviewer, String reason) => Future<void> reject(String id, String reviewer, String reason) =>
_client.reject(approvalId: id, reviewer: reviewer, reason: reason); _client.reject(approvalId: id, reviewer: reviewer, reason: reason);
/// Every tracked detached invocation (newest-first). Optionally /// Detached-runs monitor snapshot: every tracked invocation
/// scoped to one [project]. Empty when detached invocations are /// (newest-first, optionally scoped to one [project]) plus whether
/// disabled or none have run this process. /// the operator enabled the feature at all so the empty state
Future<List<DetachedRun>> listDetachedRuns({String project = ''}) async { /// can say "switched off" vs. "on, but no runs yet" truthfully.
final entries = await _client.listInvocations(); Future<({List<DetachedRun> runs, bool enabled})> listDetachedRuns({
final runs = entries String project = '',
}) async {
final r = await _client.listInvocationsFull();
final runs = r.invocations
.where((e) => project.isEmpty || e.project == project) .where((e) => project.isEmpty || e.project == project)
.map(DetachedRun.fromEntry) .map(DetachedRun.fromEntry)
.toList(); .toList();
return runs; return (runs: runs, enabled: r.detachedEnabled);
} }
/// Cancel a running/pending detached invocation. Returns true when /// Cancel a running/pending detached invocation. Returns true when

View file

@ -1744,6 +1744,7 @@
"runsReloadTooltip": "Lauf-Liste neu laden", "runsReloadTooltip": "Lauf-Liste neu laden",
"runsEmptyTitle": "Keine Läufe im Hintergrund", "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.", "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", "runsEmptyGuideButton": "Anleitung öffnen",
"runsCancelButton": "Abbrechen", "runsCancelButton": "Abbrechen",
"runsCancelSignalled": "Abbruch für {flow} ausgelöst.", "runsCancelSignalled": "Abbruch für {flow} ausgelöst.",

View file

@ -1783,6 +1783,7 @@
"runsReloadTooltip": "Reload the runs list", "runsReloadTooltip": "Reload the runs list",
"runsEmptyTitle": "No background runs", "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.", "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", "runsEmptyGuideButton": "Open the guide",
"runsCancelButton": "Cancel", "runsCancelButton": "Cancel",
"runsCancelSignalled": "Cancel signalled for {flow}.", "runsCancelSignalled": "Cancel signalled for {flow}.",

View file

@ -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.'** /// **'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; 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. /// No description provided for @runsEmptyGuideButton.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -3135,6 +3135,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get runsEmptyHint => 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.'; '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 @override
String get runsEmptyGuideButton => 'Anleitung öffnen'; String get runsEmptyGuideButton => 'Anleitung öffnen';

View file

@ -3136,6 +3136,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get runsEmptyHint => 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.'; '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 @override
String get runsEmptyGuideButton => 'Open the guide'; String get runsEmptyGuideButton => 'Open the guide';

View file

@ -24,6 +24,7 @@ class RunsPage extends StatefulWidget {
class _RunsPageState extends State<RunsPage> { class _RunsPageState extends State<RunsPage> {
List<DetachedRun> _runs = const []; List<DetachedRun> _runs = const [];
bool _detachedEnabled = false;
String? _error; String? _error;
bool _loaded = false; bool _loaded = false;
Timer? _poll; Timer? _poll;
@ -49,12 +50,13 @@ class _RunsPageState extends State<RunsPage> {
Future<void> _refresh() async { Future<void> _refresh() async {
try { try {
final runs = await HubService.instance.listDetachedRuns( final snapshot = await HubService.instance.listDetachedRuns(
project: Workspace.instance.activeSlug, project: Workspace.instance.activeSlug,
); );
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_runs = runs; _runs = snapshot.runs;
_detachedEnabled = snapshot.enabled;
_error = null; _error = null;
_loaded = true; _loaded = true;
}); });
@ -126,7 +128,10 @@ class _RunsPageState extends State<RunsPage> {
? ChainEmptyState( ? ChainEmptyState(
icon: Icons.rocket_launch_outlined, icon: Icons.rocket_launch_outlined,
title: l.runsEmptyTitle, 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 guide carries the plain-language explanation plus
// the exact operator steps (config snippet) a click // the exact operator steps (config snippet) a click
// target instead of a raw config key in the hint. // target instead of a raw config key in the hint.