feat(studio): first-run UX, recovery affordances, l10n, bundled fonts

- Connection-aware Welcome: when the hub is down, show a hero with a
  primary "Start hub" CTA + install fallback instead of a dead,
  all-unchecked onboarding checklist (the first-run cliff).
- Actionable binary-not-found (file picker + install link, not a
  "set FAI_BIN" dead end) and a connect-failure banner after
  repeated failed health polls.
- Localize six hardcoded English error/toast clusters (DE+EN ARB).
- Bundle Inter + JetBrains Mono as assets; drop the runtime
  google_fonts fetch (air-gap / KRITIS safe, no font-swap flash).

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-11 23:52:18 +02:00
parent 7511867774
commit 5313266cc4
25 changed files with 1231 additions and 234 deletions

View file

@ -18,9 +18,61 @@
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
/// Sentinel returned in `_runFai(...).stderr` when no `fai`
/// binary could be located. UI layers should not display this
/// verbatim they check [SystemActions.faiBinaryExists] first
/// and render a localized, actionable message instead.
const String kFaiBinaryNotFound = 'fai-binary-not-found';
/// Getting-started / install guide for the FI platform. Opened
/// in the OS browser as the fallback affordance when no `fai`
/// binary can be located on the machine.
const String kFaiInstallDocsUrl =
'https://git.flemming.ai/fai/platform#installation';
class SystemActions {
SystemActions._();
/// SharedPreferences key for an operator-chosen `fai` binary
/// path (set via the file picker when auto-detection fails).
static const String _kFaiBinaryPrefKey = 'system.fai_binary_path';
/// In-memory cache of the operator override, loaded once at
/// startup by [loadFaiBinaryOverride]. Null = no override.
static String? _faiBinaryOverride;
/// Restore the persisted `fai` binary override (if any) so it
/// is available before the first daemon action. Call once at
/// app start, alongside the other persisted-state loaders.
static Future<void> loadFaiBinaryOverride() async {
final prefs = await SharedPreferences.getInstance();
final p = prefs.getString(_kFaiBinaryPrefKey);
if (p != null && p.isNotEmpty && File(p).existsSync()) {
_faiBinaryOverride = p;
}
}
/// Persist an operator-chosen `fai` binary path. Used by the
/// "Locate `fai` binary…" affordance when auto-detection on
/// PATH and the canonical install dir both come up empty.
/// Returns false (and persists nothing) when [path] is empty
/// or does not point at an existing file.
static Future<bool> setFaiBinaryPath(String path) async {
if (path.isEmpty || !File(path).existsSync()) return false;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kFaiBinaryPrefKey, path);
_faiBinaryOverride = path;
return true;
}
/// True when a `fai` binary can be located (override, PATH, or
/// the canonical install dir). UI uses this to decide between
/// the normal daemon-control affordances and the "locate the
/// binary / read the install guide" recovery path.
static bool faiBinaryExists() => _faiExecutable() != null;
/// Ask the OS to open [path] in the default handler. On macOS
/// this opens text files in TextEdit, configs in the registered
/// editor, etc. Returns true on a clean spawn (process exited
@ -149,13 +201,11 @@ class SystemActions {
) async {
final exe = _faiExecutable();
if (exe == null) {
return (
ok: false,
stdout: '',
stderr:
'Could not locate the `fai` binary. Install via the '
'platform installer or set FAI_BIN to its full path.',
);
// Sentinel, not a user-facing string. Callers detect this
// via `faiBinaryExists()` and render a localized,
// actionable recovery message (locate binary / install
// guide) instead of CLI jargon like "set FAI_BIN".
return (ok: false, stdout: '', stderr: kFaiBinaryNotFound);
}
try {
final r = await Process.run(exe, args);
@ -175,9 +225,17 @@ class SystemActions {
return (executable: 'xdg-open', args: const []);
}
/// Locate the `fai` binary. Order: $FAI_BIN, PATH, fallback to
/// the canonical install location under the user's home dir.
/// Locate the `fai` binary. Order: operator override (set via
/// the file picker), $FAI_BIN, PATH, fallback to the canonical
/// install location under the user's home dir.
static String? _faiExecutable() {
final override = _faiBinaryOverride;
if (override != null &&
override.isNotEmpty &&
File(override).existsSync()) {
return override;
}
final fromEnv = Platform.environment['FAI_BIN'];
if (fromEnv != null && fromEnv.isNotEmpty && File(fromEnv).existsSync()) {
return fromEnv;