fix(studio): live-search exception, daemon-card width, featured tile size; add Start-hub button (v0.20.1)

Three reported bugs and one onramp gap:

- Store live-search threw `setState() callback returned a
  Future`. The `=>` body of `_runSearch` evaluated to the
  Future returned by `_load()`. Switched to a block body so
  the closure resolves to void; the future itself is still
  awaited by the FutureBuilder.

- Doctor's Daemon-control card was visibly narrower than its
  siblings — its only stretching child was a Wrap of small
  buttons. Wrapped the content in `SizedBox(width: infinity)`
  so the card fills the section's width like every other
  panel. Card now leads with the live `running on local:
  127.0.0.1:50051` status row + status pill so operators see
  state at a glance, then offers Restart / Start (when
  stopped) / Stop / Status.

- Featured tiles were 320×152 against the regular grid's
  ~360×168, so the editorial picks looked subordinate to
  the dense grid. Bumped to 480×220 with bigger icon, larger
  title, larger tagline so the strip reads as a hero band.

- ConnectionPill in the sidebar now offers an inline "Start
  hub" button when the daemon is unreachable. Spawns
  `fai daemon start` via SystemActions; the shell-level
  health poll picks the daemon up on its next tick. Closes
  the chicken-and-egg for Windows users who can't open a
  shell to bootstrap.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-07 21:27:09 +02:00
parent 26691b7471
commit fb7352d182
4 changed files with 256 additions and 96 deletions

View file

@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'data/hub.dart';
import 'data/system_actions.dart';
import 'pages/approvals.dart';
import 'pages/audit.dart';
import 'pages/doctor.dart';
@ -23,7 +24,7 @@ import 'widgets/widgets.dart';
/// Studio's own build version. Bump on every UI commit so the
/// running app self-identifies visible in the sidebar header
/// and quick-glance proof that you're seeing the current build.
const String kStudioVersion = '0.20.0';
const String kStudioVersion = '0.20.1';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -338,6 +339,19 @@ class _Sidebar extends StatelessWidget {
child: _ConnectionPill(
connected: connected,
label: endpointLabel,
onStartRequested: () async {
final r = await SystemActions.faiDaemon(['start']);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
r.ok
? 'Daemon start requested. Reconnecting…'
: 'Could not start daemon: ${r.stderr.trim().isEmpty ? r.stdout.trim() : r.stderr.trim()}',
),
),
);
},
),
),
if (activeChannel != null && activeChannel!.isNotEmpty) ...[
@ -482,8 +496,17 @@ class _ChannelPill extends StatelessWidget {
class _ConnectionPill extends StatelessWidget {
final bool? connected;
final String label;
/// Triggered by the inline "Start hub" button when the
/// daemon is unreachable. Runs `fai daemon start` server-side
/// (well, locally Studio shells out). The shell-level
/// health poll picks the daemon up on its next tick.
final Future<void> Function() onStartRequested;
const _ConnectionPill({required this.connected, required this.label});
const _ConnectionPill({
required this.connected,
required this.label,
required this.onStartRequested,
});
@override
Widget build(BuildContext context) {
@ -536,6 +559,31 @@ class _ConnectionPill extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant,
),
),
// Inline "Start hub" affordance when the daemon
// isn't there. Resolves the chicken-and-egg for
// Windows operators who can't run `fai daemon start`
// from a shell.
if (connected == false) ...[
const SizedBox(height: FaiSpace.sm),
SizedBox(
width: double.infinity,
child: FilledButton.tonalIcon(
onPressed: onStartRequested,
icon: const Icon(Icons.play_arrow, size: 14),
label: const Text(
'Start hub',
style: TextStyle(fontSize: 11),
),
style: FilledButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(
horizontal: FaiSpace.sm,
vertical: 4,
),
),
),
),
],
],
),
);