feat(studio): operator QoL — flow runnability, audit grouping, batch approvals, welcome celebration (v0.42.0)

Four UX threads stitched into one commit. Each pulls Studio
toward Stefan's "zero-learning-curve" goal — the feedback
that earned its own memory entry.

1. Flow-Runnability-Indikator
   ─────────────────────────
   The Flows tab now fetches `listFlows` and `listModules`
   in parallel. Each card compares the flow's
   `requiredCapabilities` against the installed-modules'
   capability set; rows with missing modules show a "Needs:
   text.extract@^0" red pill row beneath the path and have
   their Run button greyed out + tooltip
   "Install the missing modules first." Operators stop
   hitting Run → cryptic hub error → frustration.

2. Welcome-Checklist Celebration
   ─────────────────────────────
   Once all four checklist signals flip to done, an
   `_AllDoneCelebration` card replaces the bare
   "All four steps complete" + Hide button. Three concrete
   next-threads with action buttons: "Read the audit log",
   "Set up the daily Today story" (opens the Flows / Today
   doc inline via `_DocReaderSheet`), and "Build your own
   module" (opens the architecture doc). Operator who just
   got set up sees what to do next instead of an empty
   "what now?" feeling.

3. Audit-Page Time-Bucket Headers + Flow-Run Detail
   ────────────────────────────────────────────────
   The flat event list grows tiny "TODAY / YESTERDAY /
   EARLIER THIS WEEK / OLDER" section headers — bucket is
   computed in the operator's local timezone so an event at
   23:55 yesterday in Berlin doesn't end up in "today"
   because UTC happened to spill into a new day.
   Plus: the event-detail dialog gains a "View flow run"
   action when the picked event has a `flow_execution`. It
   opens a drill-down that lists every event in the
   already-fetched 100-event window sharing the same
   execution id, sorted ascending — the operator reads the
   run from step.started top to flow.completed bottom.

4. Approvals-Batch-Aktionen
   ────────────────────────
   Each pending approval card grows a checkbox. When ≥1
   selected, a floating action bar appears at the bottom
   with "N selected · Select all · Clear · Reject all ·
   Approve all". The parent loops sequentially through the
   per-record SDK calls so a partial failure produces
   "X done, Y failed" instead of a confusing all-or-nothing
   rollback. Reject prompts for a reason once and applies
   to the whole picked set.

13 new ARB keys cover the strings the four features
needed. Studio's tests stay green.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-09 11:24:57 +02:00
parent f90d8cc7a7
commit 2002486828
13 changed files with 1215 additions and 64 deletions

View file

@ -535,28 +535,151 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
),
if (allDone) ...[
const SizedBox(height: FaiSpace.md),
Row(
children: [
Icon(
Icons.check_circle,
size: 16,
color: FaiColors.success,
_AllDoneCelebration(onDismiss: _dismiss),
],
],
),
);
}
}
/// Shown beneath the four-step checklist once every signal
/// flips to done. Three concrete next-threads the operator
/// can pull on, plus the dismiss button. Each suggestion
/// links somewhere meaningful inside Studio (Audit page,
/// Today doc, Flows doc) no external browser calls.
class _AllDoneCelebration extends StatelessWidget {
final VoidCallback onDismiss;
const _AllDoneCelebration({required this.onDismiss});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l = AppLocalizations.of(context)!;
return Container(
padding: const EdgeInsets.all(FaiSpace.lg),
decoration: BoxDecoration(
color: FaiColors.success.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(FaiRadius.md),
border: Border.all(
color: FaiColors.success.withValues(alpha: 0.35),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.celebration_outlined, color: FaiColors.success),
const SizedBox(width: FaiSpace.sm),
Expanded(
child: Text(
l.welcomeChecklistAllSetTitle,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(width: FaiSpace.xs),
),
TextButton(
onPressed: onDismiss,
child: Text(l.welcomeChecklistDismiss),
),
],
),
const SizedBox(height: 4),
Text(
l.welcomeChecklistAllSetBody,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: FaiSpace.md),
_NextStepRow(
icon: Icons.timeline_outlined,
title: l.welcomeChecklistNextAuditTitle,
body: l.welcomeChecklistNextAuditBody,
buttonLabel: l.welcomeChecklistNextAuditButton,
doc: null,
onOpenDoc: null,
),
_NextStepRow(
icon: Icons.auto_awesome,
title: l.welcomeChecklistNextTodayTitle,
body: l.welcomeChecklistNextTodayBody,
buttonLabel: l.welcomeChecklistNextTodayButton,
doc: _kDocs.firstWhere(
(d) => d.slug == 'flows',
orElse: () => _kDocs.first,
),
onOpenDoc: (entry) => _DocReaderSheet.show(context, entry),
),
_NextStepRow(
icon: Icons.extension_outlined,
title: l.welcomeChecklistNextModuleTitle,
body: l.welcomeChecklistNextModuleBody,
buttonLabel: l.welcomeChecklistNextModuleButton,
doc: _kDocs.first,
onOpenDoc: (entry) => _DocReaderSheet.show(context, entry),
),
],
),
);
}
}
class _NextStepRow extends StatelessWidget {
final IconData icon;
final String title;
final String body;
final String buttonLabel;
final _DocEntry? doc;
final void Function(_DocEntry)? onOpenDoc;
const _NextStepRow({
required this.icon,
required this.title,
required this.body,
required this.buttonLabel,
required this.doc,
required this.onOpenDoc,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final canActivate = doc != null && onOpenDoc != null;
return Padding(
padding: const EdgeInsets.only(bottom: FaiSpace.sm),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: theme.colorScheme.primary),
const SizedBox(width: FaiSpace.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l.welcomeChecklistAllDone,
title,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
body,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const Spacer(),
FilledButton.tonal(
onPressed: _dismiss,
child: Text(l.welcomeChecklistDismiss),
),
],
),
],
),
const SizedBox(width: FaiSpace.md),
OutlinedButton(
onPressed: canActivate ? () => onOpenDoc!(doc!) : null,
child: Text(buttonLabel),
),
],
),
);