chain-studio/lib/data/error_presentation.dart
flemming-it 0f93d90963
Some checks failed
Security / Security check (push) Failing after 2s
fix(studio): chain config dir + spawn path + doc URLs
Track the platform rename: the hub spawn path is now ~/.chain/bin/chain
(was ~/.fai/bin/fai.exe on Windows — both dir and binary were stale, so
Studio could not launch the hub after the config-dir rename), the
~/.fai/* help strings become ~/.chain/*, FAI_REGISTRY_TOKEN ->
CHAIN_REGISTRY_TOKEN, and the two in-app doc URLs point at the public
fai/chain repo (fai/platform was renamed to the private fai/chain-private).
The .fai module bundle extension is left unchanged (format phase).
flutter analyze: no issues.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-06-16 09:38:30 +02:00

114 lines
3.7 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/fai_error_box.dart';
import '../theme/tokens.dart';
import 'fai_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.
FaiLog.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: FaiSpace.sm,
vertical: FaiSpace.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: FaiSpace.xs),
],
FaiErrorBox(error: error, isError: true),
],
),
),
),
);
}
/// 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 {
FaiLog.instance.error(source, error);
return showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: title == null ? null : Text(title),
content: SizedBox(
width: 480,
child: FaiErrorBox(error: error, isError: true, maxHeight: 240),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).maybePop(),
child: const Text('OK'),
),
],
),
);
}