fix(studio): audit batch — debug.echo install, copyable errors, fast Diagnose
From a live-test audit:
- Install via the flow-editor 'Fix' sent 'debug.echo@^0' (version
constraint included) as the install source; the hub resolves by bare
name so it missed ('no store entry for debug.echo@^0'). Strip the
@<constraint> like the Store page does — debug.echo now installs.
- Errors the operator could not copy: route the daemon-start failure
(its stderr!), the flow-editor install failure and the federation
issue failure through showFaiErrorSnack / a new showFaiProcessError
helper, so every error is selectable, one-tap copyable and logged.
- Diagnose page opened slowly because doctor() Future.wait-ed on a live
manifest fetch (8s server timeout); cap that one check at 2s so the
page no longer waits on the network.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
ee4a0f6c62
commit
5f56abcf5f
5 changed files with 85 additions and 30 deletions
|
|
@ -85,6 +85,30 @@ void showFaiErrorSnack(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Surface a failed external-process result (daemon start, binary
|
||||||
|
/// probe, file-open) as a copyable error snack. The `ok/stdout/stderr`
|
||||||
|
/// record these actions return is not a thrown error, so call sites used
|
||||||
|
/// to drop it into a raw `SnackBar(Text(...))` — uncopyable, the exact
|
||||||
|
/// thing the operator needs to paste back. This joins stderr + stdout
|
||||||
|
/// into one selectable, one-tap-copyable, logged message.
|
||||||
|
void showFaiProcessError(
|
||||||
|
BuildContext context,
|
||||||
|
String source,
|
||||||
|
String stdout,
|
||||||
|
String stderr, {
|
||||||
|
String? title,
|
||||||
|
}) {
|
||||||
|
final detail = [stderr.trim(), stdout.trim()]
|
||||||
|
.where((s) => s.isNotEmpty)
|
||||||
|
.join('\n\n');
|
||||||
|
showFaiErrorSnack(
|
||||||
|
context,
|
||||||
|
source,
|
||||||
|
detail.isEmpty ? 'process exited non-zero (no output)' : detail,
|
||||||
|
title: title,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Show [error] as a centered modal dialog. Use this when the
|
/// Show [error] as a centered modal dialog. Use this when the
|
||||||
/// failure blocks meaningful interaction (auth missing, hub
|
/// failure blocks meaningful interaction (auth missing, hub
|
||||||
/// unreachable) — a SnackBar would be too easy to dismiss.
|
/// unreachable) — a SnackBar would be too easy to dismiss.
|
||||||
|
|
|
||||||
|
|
@ -902,7 +902,18 @@ class HubService {
|
||||||
.catchError((_) => <PendingApprovalEntry>[]),
|
.catchError((_) => <PendingApprovalEntry>[]),
|
||||||
_client.verifyEventChain().catchError((_) => VerifyEventChainResponse()),
|
_client.verifyEventChain().catchError((_) => VerifyEventChainResponse()),
|
||||||
_client.listServices().catchError((_) => <DeclaredService>[]),
|
_client.listServices().catchError((_) => <DeclaredService>[]),
|
||||||
_client.checkUpdate().catchError((_) => CheckUpdateResponse()),
|
// checkUpdate does a live manifest fetch (server-side 8s timeout).
|
||||||
|
// The other five are local-socket RPCs that return in ms, so cap
|
||||||
|
// this one tightly — otherwise it alone gates the whole Diagnose
|
||||||
|
// page open. A timeout reads as "release host unreachable", which
|
||||||
|
// the UI already renders as an offline banner, not an error.
|
||||||
|
_client
|
||||||
|
.checkUpdate()
|
||||||
|
.timeout(
|
||||||
|
const Duration(seconds: 2),
|
||||||
|
onTimeout: () => CheckUpdateResponse(),
|
||||||
|
)
|
||||||
|
.catchError((_) => CheckUpdateResponse()),
|
||||||
_client.daemonPaths().catchError((_) => DaemonPathsResponse()),
|
_client.daemonPaths().catchError((_) => DaemonPathsResponse()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'data/chain_log.dart';
|
import 'data/chain_log.dart';
|
||||||
|
import 'data/error_presentation.dart';
|
||||||
import 'data/hub.dart';
|
import 'data/hub.dart';
|
||||||
import 'data/system_actions.dart';
|
import 'data/system_actions.dart';
|
||||||
import 'data/theme_plugin.dart';
|
import 'data/theme_plugin.dart';
|
||||||
|
|
@ -270,14 +271,21 @@ class StudioShellState extends State<StudioShell> {
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
final r = await SystemActions.chainDaemon(['start']);
|
final r = await SystemActions.chainDaemon(['start']);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final detail = r.stderr.trim().isEmpty ? r.stdout.trim() : r.stderr.trim();
|
if (r.ok) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(l.daemonStartRequested)),
|
||||||
content: Text(
|
);
|
||||||
r.ok ? l.daemonStartRequested : l.daemonStartFailed(detail),
|
} else {
|
||||||
),
|
// Surface the daemon's stderr/stdout as a copyable error — this
|
||||||
),
|
// is the message the operator most often needs to paste back.
|
||||||
);
|
showFaiProcessError(
|
||||||
|
context,
|
||||||
|
'daemon.start',
|
||||||
|
r.stdout,
|
||||||
|
r.stderr,
|
||||||
|
title: l.daemonStartFailed(''),
|
||||||
|
);
|
||||||
|
}
|
||||||
// Probe again shortly so the UI flips to "connected" without
|
// Probe again shortly so the UI flips to "connected" without
|
||||||
// waiting for the next 5 s tick.
|
// waiting for the next 5 s tick.
|
||||||
Future.delayed(const Duration(seconds: 1), _checkHealth);
|
Future.delayed(const Duration(seconds: 1), _checkHealth);
|
||||||
|
|
@ -690,14 +698,21 @@ class _SidebarState extends State<_Sidebar>
|
||||||
final l = AppLocalizations.of(context)!;
|
final l = AppLocalizations.of(context)!;
|
||||||
final r = await SystemActions.chainDaemon(['start']);
|
final r = await SystemActions.chainDaemon(['start']);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
final detail = r.stderr.trim().isEmpty ? r.stdout.trim() : r.stderr.trim();
|
if (r.ok) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(l.daemonStartRequested)),
|
||||||
content: Text(
|
);
|
||||||
r.ok ? l.daemonStartRequested : l.daemonStartFailed(detail),
|
} else {
|
||||||
),
|
// Surface the daemon's stderr/stdout as a copyable error — this
|
||||||
),
|
// is the message the operator most often needs to paste back.
|
||||||
);
|
showFaiProcessError(
|
||||||
|
context,
|
||||||
|
'daemon.start',
|
||||||
|
r.stdout,
|
||||||
|
r.stderr,
|
||||||
|
title: l.daemonStartFailed(''),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import '../data/hub.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../theme/theme.dart';
|
import '../theme/theme.dart';
|
||||||
import '../theme/tokens.dart';
|
import '../theme/tokens.dart';
|
||||||
|
import '../data/error_presentation.dart';
|
||||||
import '../widgets/widgets.dart';
|
import '../widgets/widgets.dart';
|
||||||
import 'welcome.dart' show showFaiDoc;
|
import 'welcome.dart' show showFaiDoc;
|
||||||
|
|
||||||
|
|
@ -48,9 +49,8 @@ class _FederationPageState extends State<FederationPage> {
|
||||||
_refresh();
|
_refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
showFaiErrorSnack(context, 'federation.issue', e,
|
||||||
SnackBar(content: Text(l.federationIssueFailed(e.toString()))),
|
title: l.federationIssueFailed(''));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart';
|
import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../data/error_presentation.dart';
|
||||||
import '../data/flow_run_driver.dart';
|
import '../data/flow_run_driver.dart';
|
||||||
import '../data/hub.dart';
|
import '../data/hub.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
@ -94,7 +95,13 @@ class _FlowsPageState extends State<FlowsPage> {
|
||||||
/// list so the editor can re-analyze without waiting for the
|
/// list so the editor can re-analyze without waiting for the
|
||||||
/// parent widget rebuild.
|
/// parent widget rebuild.
|
||||||
Future<List<String>?> _onInstallCapability(String capability) async {
|
Future<List<String>?> _onInstallCapability(String capability) async {
|
||||||
return _runInstall(source: capability);
|
// The analyzer hands us the full `use:` value (e.g. `debug.echo@^0`).
|
||||||
|
// The hub resolves install sources by bare capability name; the
|
||||||
|
// `@<constraint>` suffix would make the store lookup miss ("no store
|
||||||
|
// entry for 'debug.echo@^0'"). Strip it — same as the Store page.
|
||||||
|
final at = capability.indexOf('@');
|
||||||
|
final bare = at < 0 ? capability : capability.substring(0, at);
|
||||||
|
return _runInstall(source: bare);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `Add source for <cap>…` handler. Prompts the operator
|
/// `Add source for <cap>…` handler. Prompts the operator
|
||||||
|
|
@ -123,15 +130,13 @@ class _FlowsPageState extends State<FlowsPage> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
final l = AppLocalizations.of(context);
|
final l = AppLocalizations.of(context);
|
||||||
final msg = l != null
|
// Copyable error (was a plain SnackBar) — install failures
|
||||||
? l.installFailed(e.toString())
|
// carry the real cause (network, signature, store lookup).
|
||||||
: 'Install failed: $e';
|
showFaiErrorSnack(
|
||||||
ScaffoldMessenger.of(context).removeCurrentSnackBar();
|
context,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
'flows.install',
|
||||||
SnackBar(
|
e,
|
||||||
content: Text(msg),
|
title: l != null ? l.installFailed('') : 'Install failed',
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue