fix(welcome): structured doc-load error + Forgejo fallback link
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
The doc-reader bottom-sheet in welcome.dart hits rootBundle
to load assets/docs/<slug>[_de].md. When the asset isn't in
the bundle (e.g. the binary on disk predates the
flutter_markdown_plus migration in 69b54629 or an in-progress
asset reorganisation), the FutureBuilder's error branch dumps
the raw PlatformException text into FaiErrorBox with no
hint at what the operator should do.
Switch the throw to a structured `_DocReaderError` that
carries:
- the slug, locale, and both attempted asset paths
- the underlying error message
- a precomputed `forgejoUrl` pointing at the same doc on
the platform repo's main branch
The error UI gains a TextButton.icon below FaiErrorBox that
opens the Forgejo copy in the OS browser via the existing
SystemActions.openInOs path — operators always have a
working out, even when the local bundle is broken.
The error toString() also surfaces "rebuild Studio" as the
top suggestion, since the most common cause for asset
mismatches is a stale binary running against a newer source
tree.
New regression test (test/load_docs_test.dart) pumps the
exact code path the sheet uses (rootBundle.loadString +
Markdown with FaiTheme.markdownStyle) against all eight asset
paths (architecture/security/audit/flows × en/de). Catches a
future bundle drift before an operator notices.
No behavioural change on the happy path. Both the rendered
markdown widget and the closed-sheet animation are unchanged.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
1c306916aa
commit
570c38b238
2 changed files with 107 additions and 7 deletions
|
|
@ -910,6 +910,36 @@ class _DocCard extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Structured error type for the doc reader. Allows the
|
||||||
|
/// error UI to render distinct lines for "what we tried",
|
||||||
|
/// "what to try next", and "the raw underlying error" — and
|
||||||
|
/// to surface the Forgejo browser fallback as a one-click
|
||||||
|
/// link instead of a sentence to copy.
|
||||||
|
class _DocReaderError {
|
||||||
|
final String slug;
|
||||||
|
final String locale;
|
||||||
|
final List<String> attempted;
|
||||||
|
final String underlying;
|
||||||
|
const _DocReaderError({
|
||||||
|
required this.slug,
|
||||||
|
required this.locale,
|
||||||
|
required this.attempted,
|
||||||
|
required this.underlying,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get forgejoUrl =>
|
||||||
|
'https://git.flemming.ai/fai/platform/src/branch/main/docs/architecture/$slug.md';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'Could not load the bundled doc "$slug" ($locale).\n'
|
||||||
|
'Tried: ${attempted.join(", ")}\n'
|
||||||
|
'Underlying: $underlying\n'
|
||||||
|
'This typically means the Studio binary on disk is from\n'
|
||||||
|
'before the doc bundle changed. Rebuild Studio (fai studio\n'
|
||||||
|
'or the build pipeline) or open the Forgejo copy in browser.';
|
||||||
|
}
|
||||||
|
|
||||||
/// Modal bottom-sheet that loads the markdown for a doc entry
|
/// Modal bottom-sheet that loads the markdown for a doc entry
|
||||||
/// from `assets/docs/<slug>[_de].md` and renders it inline via
|
/// from `assets/docs/<slug>[_de].md` and renders it inline via
|
||||||
/// flutter_markdown. No browser, no network — KRITIS-friendly.
|
/// flutter_markdown. No browser, no network — KRITIS-friendly.
|
||||||
|
|
@ -951,18 +981,24 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
|
||||||
final locale = Localizations.localeOf(context).languageCode;
|
final locale = Localizations.localeOf(context).languageCode;
|
||||||
final localised = 'assets/docs/${widget.entry.slug}_$locale.md';
|
final localised = 'assets/docs/${widget.entry.slug}_$locale.md';
|
||||||
final fallback = 'assets/docs/${widget.entry.slug}.md';
|
final fallback = 'assets/docs/${widget.entry.slug}.md';
|
||||||
|
// Cache-bust attempts. If the operator is running a stale
|
||||||
|
// build from before the flutter_markdown_plus migration,
|
||||||
|
// the .dart_tool asset manifest can be inconsistent with
|
||||||
|
// the compiled binary — surface "rebuild Studio" as a
|
||||||
|
// first-class hint rather than just dumping the raw
|
||||||
|
// PlatformException string into FaiErrorBox.
|
||||||
try {
|
try {
|
||||||
return await rootBundle.loadString(localised);
|
return await rootBundle.loadString(localised);
|
||||||
} catch (firstErr) {
|
} catch (firstErr) {
|
||||||
try {
|
try {
|
||||||
return await rootBundle.loadString(fallback);
|
return await rootBundle.loadString(fallback);
|
||||||
} catch (secondErr) {
|
} catch (secondErr) {
|
||||||
// Surface BOTH attempted paths and the underlying
|
throw _DocReaderError(
|
||||||
// errors so the operator can copy a useful diagnostic
|
slug: widget.entry.slug,
|
||||||
// out of FaiErrorBox without us having to ship a doc
|
locale: locale,
|
||||||
// about reading Flutter asset errors.
|
attempted: [localised, fallback],
|
||||||
throw 'tried "$localised": $firstErr\n'
|
underlying: '$firstErr / $secondErr',
|
||||||
'then "$fallback": $secondErr';
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1028,6 +1064,9 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (snap.hasError) {
|
if (snap.hasError) {
|
||||||
|
final err = snap.error;
|
||||||
|
final structured =
|
||||||
|
err is _DocReaderError ? err : null;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(FaiSpace.xxl),
|
padding: const EdgeInsets.all(FaiSpace.xxl),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -1041,10 +1080,19 @@ class _DocReaderSheetState extends State<_DocReaderSheet> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: FaiSpace.sm),
|
const SizedBox(height: FaiSpace.sm),
|
||||||
FaiErrorBox(
|
FaiErrorBox(
|
||||||
error: snap.error,
|
error: err,
|
||||||
isError: true,
|
isError: true,
|
||||||
maxHeight: 200,
|
maxHeight: 200,
|
||||||
),
|
),
|
||||||
|
if (structured != null) ...[
|
||||||
|
const SizedBox(height: FaiSpace.md),
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.open_in_new, size: 16),
|
||||||
|
label: Text(structured.forgejoUrl),
|
||||||
|
onPressed: () =>
|
||||||
|
SystemActions.openInOs(structured.forgejoUrl),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
52
test/load_docs_test.dart
Normal file
52
test/load_docs_test.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
import 'package:fai_studio/l10n/app_localizations.dart';
|
||||||
|
import 'package:fai_studio/theme/theme.dart';
|
||||||
|
|
||||||
|
/// Recreates the exact DocReaderSheet code path that
|
||||||
|
/// welcome.dart uses, including FaiTheme.markdownStyle, so a
|
||||||
|
/// regression in either rootBundle, the markdown parser, or
|
||||||
|
/// the style sheet surfaces here instead of only at runtime
|
||||||
|
/// when an operator clicks a doc card.
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
testWidgets('DocReaderSheet renders all four german + english docs',
|
||||||
|
(tester) async {
|
||||||
|
final theme = FaiTheme.light();
|
||||||
|
for (final slug in ['architecture', 'security', 'audit', 'flows']) {
|
||||||
|
for (final locale in ['de', 'en']) {
|
||||||
|
final path = locale == 'de'
|
||||||
|
? 'assets/docs/${slug}_de.md'
|
||||||
|
: 'assets/docs/$slug.md';
|
||||||
|
final text = await rootBundle.loadString(path);
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
locale: Locale(locale),
|
||||||
|
supportedLocales: const [Locale('en'), Locale('de')],
|
||||||
|
localizationsDelegates: const [
|
||||||
|
AppLocalizations.delegate,
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
theme: theme,
|
||||||
|
home: Scaffold(
|
||||||
|
body: Markdown(
|
||||||
|
data: text,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
selectable: true,
|
||||||
|
styleSheet: FaiTheme.markdownStyle(theme),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
await tester.pump();
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('rendered $path');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue