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>
138 lines
4.5 KiB
Dart
138 lines
4.5 KiB
Dart
// Single entry point for surfacing an error to the operator.
|
|
// Every error pathway Studio shows (SnackBar, dialog, inline
|
|
// strip) should go through one of these helpers so the
|
|
// operator gets:
|
|
//
|
|
// • a copyable, selectable rendition of the message
|
|
// • a friendly headline (gRPC codes mapped via
|
|
// friendlyError) instead of a wall-of-stack-trace
|
|
// • an automatic append to ~/.chain/logs/studio-errors.log so
|
|
// the trail survives the dismiss action
|
|
//
|
|
// Call sites used to scatter `ScaffoldMessenger.of(ctx).showSnackBar(
|
|
// SnackBar(content: Text(e.toString())))` — that violated the
|
|
// "errors must be copyable" rule everywhere. These helpers
|
|
// replace those open-coded sites.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../widgets/chain_error_box.dart';
|
|
import '../theme/tokens.dart';
|
|
import 'chain_log.dart';
|
|
|
|
/// Show [error] as a SnackBar with a copy affordance.
|
|
///
|
|
/// [source] is a short tag (`flows.run`, `audit.clear`, …)
|
|
/// used to group entries in the on-disk log so an operator or
|
|
/// `chain doctor` can scan recent failures by area without
|
|
/// reading the full free-text. Keep it stable; treat it like
|
|
/// a metric label, not free-form text.
|
|
///
|
|
/// [context] is the build-context the snack lands on. Falls
|
|
/// back to a Material default duration of 6 seconds — twice
|
|
/// the framework default so the operator has time to click
|
|
/// copy on an unfamiliar message.
|
|
void showFaiErrorSnack(
|
|
BuildContext context,
|
|
String source,
|
|
Object error, {
|
|
String? title,
|
|
Duration duration = const Duration(seconds: 6),
|
|
}) {
|
|
// Fire-and-forget log write. If the disk's full or the dir
|
|
// is read-only the UI still surfaces the error.
|
|
ChainLog.instance.error(source, error, context: title);
|
|
final messenger = ScaffoldMessenger.maybeOf(context);
|
|
if (messenger == null) {
|
|
// No Scaffold above — the caller is likely showing the
|
|
// error from inside a dialog. We've already logged; bail
|
|
// rather than throw.
|
|
return;
|
|
}
|
|
messenger.hideCurrentSnackBar();
|
|
final theme = Theme.of(context);
|
|
messenger.showSnackBar(
|
|
SnackBar(
|
|
duration: duration,
|
|
behavior: SnackBarBehavior.floating,
|
|
// Default SnackBar padding crushes our error box; reset
|
|
// to zero and let the box's own padding take over.
|
|
padding: EdgeInsets.zero,
|
|
content: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: ChainSpace.sm,
|
|
vertical: ChainSpace.xs,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (title != null) ...[
|
|
Text(
|
|
title,
|
|
style: theme.textTheme.titleSmall?.copyWith(
|
|
color: theme.colorScheme.error,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: ChainSpace.xs),
|
|
],
|
|
ChainErrorBox(error: error, isError: true),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 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
|
|
/// failure blocks meaningful interaction (auth missing, hub
|
|
/// unreachable) — a SnackBar would be too easy to dismiss.
|
|
Future<void> showFaiErrorDialog(
|
|
BuildContext context,
|
|
String source,
|
|
Object error, {
|
|
String? title,
|
|
}) async {
|
|
ChainLog.instance.error(source, error);
|
|
return showDialog<void>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: title == null ? null : Text(title),
|
|
content: SizedBox(
|
|
width: 480,
|
|
child: ChainErrorBox(error: error, isError: true, maxHeight: 240),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).maybePop(),
|
|
child: const Text('OK'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|