feat(studio): central error helpers + inline log viewer + Today CTAs
Some checks failed
Security / Security check (push) Failing after 1s

Bundles the error-UX overhaul and the inline log viewer:

  - New `FaiLog` (`~/.fai/logs/studio-errors.log`, 256 KiB rotation,
    JSON-per-line). Every operator-visible failure is appended so
    `fai admin doctor` and the new viewer can show the trail
    without the operator having to reproduce the failure.
  - New `showFaiErrorSnack` / `showFaiErrorDialog` helpers wrap
    `FaiErrorBox` in copyable surfaces; 27 ad-hoc
    `SnackBar(content: Text(e.toString()))` sites swept to use
    them (settings, doctor, audit, store, module sheet, system-AI
    editor, flow output).
  - New `FaiLogViewer` modal (`showFaiLogViewer`) renders log
    files inline with line numbers, JSON-key + `[level]` token
    colouring, Copy-all, Refresh, Open-externally. Doctor's
    daemon-paths panel grows a "View" button next to "Open" for
    every `.log` row and now also lists the Studio errors log.
  - Today carousel: CTAs now actually re-run search after a
    `filterCategory` / `runQuery` story is tapped (was only
    flipping the chip state). Fallback story list bumped to 8.
  - Editor bumped to git ref carrying 0.15.0 (type-token
    colouring + analyzer diagnostics).

Studio bumped to 0.62.0.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-04 02:08:15 +02:00
parent 2731062a13
commit 8f5cd2528b
22 changed files with 1087 additions and 134 deletions

View file

@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import '../data/error_presentation.dart';
import '../data/hub.dart';
import '../data/system_actions.dart';
import '../data/today_story_loader.dart';
@ -310,19 +311,16 @@ class _StorePageState extends State<StorePage> {
_source.isEmpty &&
!_installedOnly;
final hasNoFederation = !raw.any((e) => e.isFederated);
// Past-onboarding signal: the operator has
// 3+ modules installed (wasm + federated
// combined). At that point the editorial
// hero stops earning its scroll cost they
// know what FI does. They can still bring
// it back via the "Tour" button (TODO) or
// by clearing _todayDismissed in prefs.
final installedCount = raw
.where((e) => e.installed)
.length;
final pastOnboarding = installedCount >= 3;
final showToday =
!_todayDismissed && isBrowsing && !pastOnboarding;
// Today carousel stays visible until the
// operator explicitly dismisses it. The earlier
// installedCount3 auto-hide killed the strip
// for anyone past first-launch meaning the
// rotating editorial stories (themes, audit,
// SDK, sandbox) were never seen by experienced
// operators, which is exactly the audience the
// wider story set is for. One-click dismiss is
// enough opt-out.
final showToday = !_todayDismissed && isBrowsing;
// Recommended-source quick-adds are inlined in
// the Today footer when both apply, so the
// operator never sees two competing editorial
@ -385,22 +383,24 @@ class _StorePageState extends State<StorePage> {
: null,
onDismiss: () =>
setState(() => _todayDismissed = true),
onCta:
onCta: _ctaCallbackFor(
_todayStories[_todayIndex % _todayStories.length],
),
// Per-story brand chips: only the MCP
// story shows DeepWiki / Semgrep
// quick-adds, so the chips don't read
// as ads on stories that aren't about
// sources. Standalone "Suggested
// sources" strip below still catches
// the dismissed-Today / no-federation
// edge case.
recommendedSources:
_todayStories[_todayIndex %
_todayStories.length]
.cta ==
TodayCta.openSettings
? () => FaiSettingsDialog.show(context)
: null,
// Quick-add chips removed from the
// editorial hero they showed the
// same DeepWiki / Semgrep names every
// session, which felt like ads. Public
// sources still live one click away
// under Settings MCP Clients, and
// the standalone "Suggested sources"
// strip below picks them up.
recommendedSources: const <_RecommendedSource>[],
TodayCta.showSuggestedSources
? _kRecommendedSources
: const <_RecommendedSource>[],
recommendedAdding: _addingSources,
onAddRecommended: _addRecommendedSource,
),
@ -554,6 +554,47 @@ class _StorePageState extends State<StorePage> {
}
}
/// Resolve a Today story's CTA descriptor into a real callback
/// the hero card can wire to its button. `null` collapses the
/// button out of the layout used for narrative-only stories
/// (audit / sandbox explainer) where there's nowhere obvious
/// to send the operator next.
VoidCallback? _ctaCallbackFor(TodayStoryData story) {
switch (story.cta) {
case TodayCta.none:
return null;
case TodayCta.openSettings:
return () => FaiSettingsDialog.show(context);
case TodayCta.filterCategory:
if (story.ctaPayload.isEmpty) return null;
return () {
// Two-step refresh: setState makes the dropdown
// header re-render with the new selection, then
// _runSearch re-issues the backend query with
// `category:` in its filter without the second
// call the grid keeps showing the unfiltered list
// and the carousel button looked broken.
setState(() => _category = story.ctaPayload);
_runSearch();
};
case TodayCta.runQuery:
if (story.ctaPayload.isEmpty) return null;
return () {
_queryCtrl.text = story.ctaPayload;
setState(() {});
_runSearch();
};
case TodayCta.showSuggestedSources:
// Stays on this slide the chips render in the hero
// footer for stories with this CTA. The button just
// scrolls focus to them; for now the chips are
// immediately visible above the fold, so the click is
// effectively a no-op acknowledgement. Keeping it
// wired avoids a dead button.
return () {};
}
}
void _clearQuery() {
setState(() {
_queryCtrl.clear();
@ -669,10 +710,11 @@ class _StorePageState extends State<StorePage> {
_runSearch();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l.storeProviderAddFailed(s.label, e.toString())),
),
showFaiErrorSnack(
context,
'store.provider.add',
e,
title: l.storeProviderAddFailed(s.label, ''),
);
} finally {
if (mounted) setState(() => _addingSources.remove(s.name));
@ -2143,9 +2185,10 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> {
final r = await SystemActions.openInOs(widget.item.repository);
if (!mounted) return;
if (!r.ok) {
final l = AppLocalizations.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l.storeOpenBrowserFailed(r.stderr))),
showFaiErrorSnack(
context,
'store.repository.open',
r.stderr.isEmpty ? 'open failed' : r.stderr,
);
}
}
@ -2926,18 +2969,18 @@ class _ScreenshotPlaceholder extends StatelessWidget {
/// on the dedicated Welcome page, not in the store carousel.
const List<TodayStoryData> _kFallbackTodayStories = <TodayStoryData>[
TodayStoryData(
badgeEn: 'MODULES',
badgeDe: 'MODULE',
titleEn: 'Modules are the building blocks',
titleDe: 'Module sind die Bausteine',
badgeEn: 'MCP',
badgeDe: 'MCP',
titleEn: 'Plug in any MCP server',
titleDe: 'Jeden MCP-Server einbinden',
bodyEn:
'Each tile in the grid below is a sandboxed WASM module. Pick one, hit Install, and the hub adds it to your local registry. Modules declare their permissions up front; the hub denies anything else.',
'F∆I speaks the Model Context Protocol out of the box. Add a public server (DeepWiki, Semgrep) or a private one — its tools show up in this grid next to the built-in modules. Same install button, same audit trail.',
bodyDe:
'Jede Kachel unten ist ein sandboxes WASM-Modul. Auswählen, Installieren klicken — der Hub fügt es deiner lokalen Registry hinzu. Module deklarieren ihre Berechtigungen explizit; der Hub verweigert alles andere.',
ctaLabelEn: '',
ctaLabelDe: '',
icon: Icons.extension_outlined,
cta: TodayCta.none,
'F∆I spricht das Model Context Protocol von Haus aus. Einen öffentlichen Server (DeepWiki, Semgrep) oder einen eigenen hinzufügen — seine Tools tauchen in diesem Grid neben den eingebauten Modulen auf. Gleicher Install-Button, gleiche Audit-Spur.',
ctaLabelEn: 'Browse suggested sources',
ctaLabelDe: 'Empfohlene Quellen ansehen',
icon: Icons.hub_outlined,
cta: TodayCta.showSuggestedSources,
),
TodayStoryData(
badgeEn: 'FLOWS',
@ -2948,24 +2991,97 @@ const List<TodayStoryData> _kFallbackTodayStories = <TodayStoryData>[
'A flow is a YAML file with three keys — inputs, steps, outputs. Each step calls one module. Every run lands in the audit log with a verifiable hash chain. Build flows graphically or write the YAML directly — either way, the run is reproducible.',
bodyDe:
'Ein Flow ist eine YAML-Datei mit drei Schlüsseln — inputs, steps, outputs. Jeder Schritt ruft ein Modul auf. Jeder Lauf landet im Audit-Log mit verifizierbarer Hash-Kette. Flows grafisch bauen oder direkt YAML schreiben — der Lauf bleibt reproduzierbar.',
ctaLabelEn: 'Show featured flows',
ctaLabelDe: 'Featured-Flows zeigen',
icon: Icons.account_tree_outlined,
cta: TodayCta.runQuery,
ctaPayload: 'hello',
),
TodayStoryData(
badgeEn: 'THEMES',
badgeDe: 'THEMES',
titleEn: 'Make Studio yours — pick a theme',
titleDe: 'Studio nach deinem Geschmack — Theme wählen',
bodyEn:
'Solarized, Sunflower, Space, Sunset, Glass-Apple, Accessibility — Studio plugins re-skin the whole app at runtime. Or pick a single accent colour and let Material 3 derive the rest. Browse the studio-plugin category for the full set.',
bodyDe:
'Solarized, Sunflower, Space, Sunset, Glass-Apple, Accessibility — Studio-Plugins themen die App zur Laufzeit komplett um. Oder eine einzelne Akzentfarbe wählen und Material 3 leitet den Rest ab. Die Kategorie Studio-Plugin zeigt das volle Set.',
ctaLabelEn: 'Show themes',
ctaLabelDe: 'Themes anzeigen',
icon: Icons.palette_outlined,
cta: TodayCta.filterCategory,
ctaPayload: 'studio-plugin',
),
TodayStoryData(
badgeEn: 'TEXT TOOLS',
badgeDe: 'TEXT-TOOLS',
titleEn: 'Extract, anonymise, translate, summarise',
titleDe: 'Extrahieren, anonymisieren, übersetzen, zusammenfassen',
bodyEn:
'The text-processing bundle is F∆I\'s document-analysis backbone. PDF and DOCX in, plain text out. Then anonymise PII, translate, or summarise — every step audit-grade, every Ollama call recorded with model digest.',
bodyDe:
'Das Text-Processing-Bündel ist F∆Is Dokumentenanalyse-Rückgrat. PDF und DOCX rein, Klartext raus. Dann PII anonymisieren, übersetzen oder zusammenfassen — jeder Schritt audit-tauglich, jeder Ollama-Aufruf mit Modell-Digest protokolliert.',
ctaLabelEn: 'Show text modules',
ctaLabelDe: 'Text-Module zeigen',
icon: Icons.article_outlined,
cta: TodayCta.filterCategory,
ctaPayload: 'text-processing',
),
TodayStoryData(
badgeEn: 'SANDBOX',
badgeDe: 'SANDBOX',
titleEn: 'Every module declares its permissions',
titleDe: 'Jedes Modul deklariert seine Berechtigungen',
bodyEn:
'F∆I modules are WASM components. They run in a sandbox with no ambient authority — every filesystem path, every network host, every env var has to be listed in module.yaml. The hub enforces them. No declaration, no access.',
bodyDe:
'F∆I-Module sind WASM-Komponenten. Sie laufen in einer Sandbox ohne Ambient-Authority — jeder Dateipfad, jeder Netzwerk-Host, jede Env-Var muss in module.yaml stehen. Der Hub setzt es durch. Keine Deklaration, kein Zugriff.',
ctaLabelEn: 'Review permissions',
ctaLabelDe: 'Berechtigungen prüfen',
icon: Icons.shield_outlined,
cta: TodayCta.openSettings,
),
TodayStoryData(
badgeEn: 'AUDIT',
badgeDe: 'AUDIT',
titleEn: 'Tamper-evident hash chain — built in',
titleDe: 'Manipulationssicher per Hash-Kette — eingebaut',
bodyEn:
'Every flow run, every install, every approval lands in ~/.fai/audit/ as a hash-chained event log. Any later edit invalidates the chain. CRA-ready out of the box — no compliance product to buy on top.',
bodyDe:
'Jeder Flow-Lauf, jede Installation, jede Freigabe landet in ~/.fai/audit/ als hash-verkettetes Event-Log. Jede spätere Änderung bricht die Kette. CRA-tauglich von Haus aus — kein Compliance-Produkt zum Aufkaufen nötig.',
ctaLabelEn: '',
ctaLabelDe: '',
icon: Icons.account_tree_outlined,
icon: Icons.timeline_outlined,
cta: TodayCta.none,
),
TodayStoryData(
badgeEn: 'EXTENSIBILITY',
badgeDe: 'ERWEITERBAR',
titleEn: 'Bring your own sources',
titleDe: 'Eigene Quellen anbinden',
badgeEn: 'TEST FIRST',
badgeDe: 'ZUERST TESTEN',
titleEn: 'fai doctor — diagnose before you ship',
titleDe: 'fai doctor — diagnostizieren bevor ausgerollt wird',
bodyEn:
'The hub federates external module sources — public MCP servers, n8n workflows, your own internal registry. Configure a source under Settings → MCP Clients and its tools appear in this store alongside the built-in modules. Same install button, same audit trail.',
'Before installing a module on a production hub, run `fai doctor` in the CLI. It validates manifest, signatures, declared permissions against the operator policy ceiling, and reports anything the hub would reject — no surprises at install time.',
bodyDe:
'Der Hub bindet externe Modul-Quellen ein — öffentliche MCP-Server, n8n-Workflows, eigene interne Registries. Eine Quelle unter Einstellungen → MCP-Clients konfigurieren, und ihre Tools erscheinen hier im Store neben den eingebauten Modulen. Gleicher Install-Button, gleiche Audit-Spur.',
ctaLabelEn: 'Manage sources',
ctaLabelDe: 'Quellen verwalten',
icon: Icons.hub_outlined,
cta: TodayCta.openSettings,
'Bevor ein Modul auf einem Produktions-Hub installiert wird, in der CLI `fai doctor` laufen lassen. Validiert Manifest, Signaturen und deklarierte Berechtigungen gegen die Operator-Policy-Decke — keine Überraschungen beim Install.',
ctaLabelEn: '',
ctaLabelDe: '',
icon: Icons.health_and_safety_outlined,
cta: TodayCta.none,
),
TodayStoryData(
badgeEn: 'BUILD ONE',
badgeDe: 'EIGENES MODUL',
titleEn: 'Build a module in a single Rust file',
titleDe: 'Ein Modul in einer einzigen Rust-Datei bauen',
bodyEn:
'fai-module-sdk gives you a #[fai::module] macro and a Cargo target. Declare inputs / outputs in module.yaml, write the function, cargo build, drop the wasm in ~/.fai/modules. The hub picks it up at next list. Polyglot SDKs follow in Phase 1.',
bodyDe:
'fai-module-sdk liefert ein #[fai::module]-Makro und ein Cargo-Target. Inputs / Outputs in module.yaml deklarieren, Funktion schreiben, cargo build, das wasm in ~/.fai/modules legen. Der Hub erkennt es beim nächsten Listing. Polyglott-SDKs folgen in Phase 1.',
ctaLabelEn: '',
ctaLabelDe: '',
icon: Icons.code_outlined,
cta: TodayCta.none,
),
];