Second half of the May-2026 trust pass. Drops the wall of
gRPC trailers from every error surface and makes the Store
honest about what is and isn't installable.
Friendly errors:
- New `friendlyError(Object, AppLocalizations)` mapper turns
GrpcError + arbitrary throwables into a localised headline,
optional recovery hint, and a verbatim detail string kept
behind a "Show details" expander. Duck-typed on `.code` /
`.message` so Studio doesn't have to depend on package:grpc
directly.
- `FaiErrorBox` gains an `error:` constructor that runs the
mapper. Every call site that used to render
`snap.error.toString()` (flows, welcome, store) switches to
it.
- 9 .arb entries per locale cover the gRPC codes we actually
emit (INVALID_ARGUMENT, NOT_FOUND, ALREADY_EXISTS,
PERMISSION_DENIED, FAILED_PRECONDITION, INTERNAL,
UNAVAILABLE, UNAUTHENTICATED) plus copy/details affordances.
- `test/friendly_error_test.dart` — 6 unit tests for the
mapper. Covers the mapping table, locale-switching, and the
non-gRPC fallback so future regressions show up in CI.
Capability discovery:
- New `HubService.allCapabilities()` reads the kind-aware
capability list (wasm + builtin + federated) and returns a
Dart-side `CapabilityInfo` value type. The flow page's
missing-dependency check uses it so `system.approval` and
federated MCP/n8n tools count as "available" — fixes the
Run button staying disabled forever.
- `HubService.listModules()` filters to kind=wasm so the
Modules page doesn't sprout synthetic "system" entries that
the operator can't uninstall.
Store clarity:
- New "Installable only" filter, on by default. Roughly 2/3
of seed entries currently carry `status: planned`; the
default view stops being noise.
- Featured-strip cards for planned modules now show a
"Coming soon" pill instead of an empty action area.
- Main-grid cards for non-installable modules dim to 60%
opacity so the eye lands on actionable cards first.
- Detail-sheet "Nicht installierbar" tooltip → inline hint
box. The reason is visible without hovering.
MCP localisation:
- `_kMcpSuggestions` no longer holds 11 hardcoded English
description strings. The `description` field is replaced
with a `resolveDescription(AppLocalizations)` lookup that
switches on the suggestion `name` to read the matching
`mcpSuggestion*Desc` .arb key. EN + DE shipped.
- New `FaiEnBadge` widget renders a small `[EN]` pill when
the active locale isn't English. Used next to MCP /
federated store entries' tagline + description because
the server supplies them in English and we can't translate
on the fly yet — the badge is the honest signal until the
planned `studio.translate` plugin lands.
Plus housekeeping: removed the unused `_keepImport` lint
escape in the test and the dangling library doc-comment in
`format.dart`.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Documentation system (the original "whole docs system is broken"
complaint):
- Studio reads inline docs from disk via the new
getInstalledModuleDocs RPC. No network, no auth, no provider
lock-in.
- store.dart probes for MODULE.md eagerly when a module-detail
sheet opens (a single file stat). The Documentation section
only renders when the bundle actually shipped docs.
- Shared FaiTheme.markdownStyle helper used by Welcome's
DocReaderSheet and Store's DocsPanel so every inline doc
reads in the same typography. The shared style forces
code.backgroundColor = transparent to suppress the per-span
bands flutter_markdown's default code style draws on dark
themes.
Flow run dialog:
- Run-button gating now strips the @version suffix from
installed capabilities before the contains() check. Without
this fix the button stayed disabled for every flow with
dependencies, even after a successful install.
- Studio derives a MIME type from the picked file's extension
(small per-suffix map; .pdf, .docx, .txt, .json, ...) and
forwards it to the hub. Fixes "unsupported MIME type:
application/octet-stream" from text.extract.
- Dialog title tracks the future's state: running -> "extract
laeuft", success -> "extract -- Ergebnis", failure ->
"extract -- fehlgeschlagen".
- Output rendering moved from String stringification to a
sealed FlowOutput type (Text / Json / Bytes / File / Unknown).
A new FaiFlowOutput widget dispatches per variant: markdown
for text/markdown (heuristic), pretty JSON for proto Struct,
inline image preview + Save-As for bytes, Open for file URIs.
Byte sizes:
- New humanBytes() helper renders 23.3 kB / 1.04 MB style values
with three significant digits, matching Finder / GNOME Files.
Wired into flow-card pills, picked-file readouts, and the
bytes-payload preview line.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
_DocReaderSheetState.initState() called _load(), which
synchronously calls Localizations.localeOf(context) before any
await. That triggers dependOnInheritedWidgetOfExactType<
_LocalizationsScope>() in initState(), which Flutter forbids
because InheritedWidget wiring is not finished yet.
Symptom (clicking a doc card in Welcome):
dependOnInheritedWidgetOfExactType<_LocalizationsScope>()
or dependOnInheritedElement() was called before
_DocReaderSheetState.initState() completed.
Fix: defer _load() to didChangeDependencies(), which runs
after initState() and after InheritedWidget hookup. Gate on
_content == null so it runs exactly once per widget lifetime
(didChangeDependencies can also fire on later dependency
changes; we only want the first load).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The welcome checklist's "Install a text module" item used:
m.any((x) => x.name.startsWith('text.'))
But ModuleSummary.name is the module identifier (text-extract,
text-summarize, …), not the capability identifier (text.extract,
text.summarize, …). The check never matched even when several
text-* modules were installed — Stefan saw the item pending with
4 text capabilities live.
Fix: check capability names instead, which carry the dot-namespace:
m.any((x) => x.capabilities.any((c) => c.startsWith('text.')))
Semantically aligned with the onboarding intent ("any text.*
capability available").
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
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>
Three concrete fixes against today's user feedback.
- Flows that take binary inputs (extract / extract-summarize /
…) work from the Flows tab. The run-flow input dialog now
accepts the same `@/path/to/file` syntax `fai run --input`
uses on the CLI: any value beginning with `@` is read as
bytes and sent as a binary Payload; plain values still flow
through as text. The dialog hint copy and the example
placeholder reflect the new syntax. File-read failures
surface as a SnackBar before the run dialog opens, so a
typo in the path doesn't reach the hub. Threaded through
`_FlowRunDialog` and `HubService.runSavedFlow`, which both
carry separate `textInputs` and `fileInputs` maps now and
forward to the SDK's mixed-mode runSavedFlow (v0.14.0).
- The Welcome doc-reader's error path uses `FaiErrorBox` so
the actual underlying error is selectable + copy-to-
clipboard via the existing widget. Plus the loader throws
a richer error string that names *both* attempted asset
paths (`<slug>_<lang>.md` and the EN fallback) and the
underlying exception each, so the operator can paste a
diagnostic into a chat without us having to ship a
separate "how to read Flutter asset errors" doc.
- Doctor's empty Services panel had a horizontal RenderFlex
overflow at narrow widths because the long mono-spaced
hint ("add to ~/.fai/config.yaml under services:") and
the leading icon+text both demanded full intrinsic width
in a single Row. Now wraps via a `Wrap` widget so the
hint flows to a second line on narrow viewports and stays
in the same row when there's space.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Three fixes against the v0.39.0 user feedback.
- Layout exception "BoxConstraints forces an infinite
height". `_PillarRow`'s wide-window path used
`Row(crossAxisAlignment: CrossAxisAlignment.stretch)`
inside a `Column` inside a `SingleChildScrollView` — Row
inherits unbounded height, can't stretch to anything,
Flutter throws. Wrapped the Row in `IntrinsicHeight` so
the Row's height is bounded to the tallest child first.
Same shape `_DocsRow`'s narrow-window path was triggering
with `Wrap` + `SizedBox(width: double.infinity)` — replaced
with a plain `Column` for that path; the wide path keeps
the Wrap with the proper finite cardWidth.
- Dropped the "WIE ES ZUSAMMENPASST" / "HOW IT FITS
TOGETHER" section header. Operator-feedback was that the
label sounded like a question without an answer. The hero
already establishes the page; the three Hub/Module/Flow
cards are self-explaining beneath it. Header was visual
clutter and the underlying ARB keys are gone.
- Reordered the sidebar: Welcome, **Store**, Doctor, Flows,
Audit, Approvals. Store is the operator's daily-use
surface and belongs above Doctor (which is a diagnostics
page). Welcome stays slot 0 as the first-launch landing.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Final slice of the Welcome surface. Four steps that prove
the operator has the basic loop wired up; each step is
a live probe against the running hub, no manual ticks.
The four checked signals:
1. System AI configured
→ `HubService.systemAiStatus().enabled`
2. Public capability source added
→ `listMcpClients()` returns at least one entry
3. Text module installed
→ `listModules()` contains a name starting with `text.`
4. Saved flow run
→ any `flow.completed` event in the last 100 audit
events
All four probes fire in parallel from `_OnboardingChecklist`'s
`_refresh()`. Failures stay false (catchError) so a hub that
is briefly unreachable doesn't blank the whole row — the
operator hits the explicit Refresh icon to retry.
UX:
- Renders between hero and pillars so the actionable path
beats the educational content for screen real estate.
- Each row: status icon (○ pending / ✓ done) + title +
one-line hint pointing the operator at the right Studio
surface + status pill.
- Done rows strike-through the title and dim it; pending
rows stay full-strength.
- When all four flip to done, an "All four steps complete"
footer appears with a `Hide checklist` button. Clicking
persists `welcome.checklist.dismissed = true` via
SharedPreferences; the section is gone for good (we don't
re-nag operators who chose a non-default path).
15 new ARB keys for the section header, body, four rows
plus done/pending pills, all-done footer, dismiss /
refresh / refreshing button labels.
Three-phase plan from `docs/landing-page-design.md` is now
fully shipped: Phase A scaffolding (v0.37.0), Phase B
embedded doc reader (v0.38.0), Phase C live checklist
(v0.39.0).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Second slice of the Welcome surface. Operator-facing
documentation now lives inside Studio as bundled assets and
renders inline via a modal sheet — no browser, no external
link, air-gap-tauglich.
- Four operator-readable explainers under `assets/docs/`,
each with an EN + DE pair:
architecture[_de].md — Hub / Module / Flow + how they fit
security[_de].md — sandbox model, declared perms,
operator ceiling
audit[_de].md — hash-chained log, WORM-1 mechanics,
`fai admin verify-events`
flows[_de].md — flow YAML, templating reference,
extract→summarize example
These are short (≈ 300-500 words each), operator-shaped
prose. Not copies of the architecture docs in
fai_platform/docs/architecture/ — those are
contributor-dense.
- pubspec.yaml declares `assets/docs/` so the markdown ships
inside the Studio binary. Air-gap deployments read them
with no network access.
- New `_DocReaderSheet` modal: 85 %-of-viewport bottom sheet,
drag handle + title bar with the doc icon and close button,
scrollable Markdown body styled to match Studio chrome.
Loads `assets/docs/<slug>_<locale>.md` first, falls back to
the EN file. Locale comes from
`Localizations.localeOf(context)`.
- `_DocsRow` on the Welcome page sits below the trust-posture
deck. Two-column grid on ≥ 640 dp, single column below.
Each card is icon + title + one-line blurb + chevron;
click opens the reader sheet for that slug.
- 12 new ARB keys for the docs section (header, blurb, four
card titles + blurbs, close button, error message).
- 4 new icons reused: `account_tree_outlined` (architecture),
`shield_outlined` (security), `verified_outlined` (audit),
`alt_route_outlined` (flows).
Implements Phase B of `docs/landing-page-design.md`. Phase C
(live getting-started checklist with persistent state) is the
last remaining slice.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
First slice of the new sidebar destination outlined in
docs/landing-page-design.md. Static content only — embedded
docs (Phase B) and the live getting-started checklist
(Phase C) follow.
Phase A:
- New `WelcomePage` at `lib/pages/welcome.dart`, registered
as sidebar slot 0 above Doctor. Default selectedIndex stays
0, so a fresh launch lands on Welcome.
- Hero card: gradient backdrop matching the Today-Hero in the
store, F∆I Platform headline, subtitle taken from CLAUDE.md
("deterministic workflow engine for AI-assisted document
processing in regulated environments").
- Three-pillar row "Hub / Module / Flow" — operator-readable
prose, not architecture-doc dense. Stacks to a column under
640 dp window width so card text never gets cropped.
- Trust-posture deck — "Sandbox by default", "Tamper-evident
audit log", "Air-gap ready". Same content that used to
rotate as carousel slides in the store; reading them as a
single deck with full prose works better than rotating
through fragments.
- AppBar follows the same `titleSpacing: FaiSpace.xl`
alignment as the Store so the title sits flush with the
body padding.
- 13 new ARB keys for the page content (EN + DE).
Smoke test now expects "Welcome" in the navigation rail; the
existing `Doctor / Store / Flows / Audit / Approvals`
expectations stay unchanged.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>