+ label opacity in lockstep (no more glitchy expand)
The previous "fixed-width icon column" commit already locked
the icon's horizontal pixel-X, but the rail still _felt_
glitchy on expand because the rail's geometry and the rail's
content were animated by two different mechanisms:
- Width: AnimatedContainer over FaiMotion.fast (120 ms)
- Content: setState(_hovered = true) → conditional
`if (expanded) Expanded(label)` snaps in one frame
So on mouse-enter the label widget appeared INSTANTLY while
the rail was still 72 px wide. The label tried to render in
0 px of available space and Flutter's layout engine clamped
it; over the next 7 frames the rail grew to 220 px and the
label visibly "settled in". That's the perceived glitch.
Replace the two-source animation with a single
SingleTickerProviderStateMixin + AnimationController whose
value `t` (0..1, eased via easeInOutCubic) drives both:
- rail width = lerp(72, 220, t)
- label opacity = t
- labelsInteractive = t > 0.5 (so hidden buttons can't
eat clicks meant for the icon column)
Labels are wrapped in `t > 0 ? IgnorePointer(Opacity(...))
: SizedBox.shrink()`. Once the animation starts, the label
joins the tree at opacity ≈ 0 (invisible — no pop) and
fades up smoothly as t grows. When fully collapsed (t == 0)
the label is removed from layout entirely, so the connection
pill's tall "Start hub" affordance doesn't inflate the
collapsed rail's height (this also fixes the widget_test
vertical-overflow that 0.51.4 introduced).
Same pattern applied to the footer (settings icon stays in
the 72-px column always; theme/lang/clock fade in beside it)
and to each _SidebarItem destination row.
End result: width AND content travel together along the same
animation curve. No more "snap then catch up" — they're
mathematically inseparable.
Version 0.51.4 → 0.51.5.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Previous "left-anchor everything via padding" approach made
the math work on paper but the perceived shift persisted. The
root cause was structural, not padding-based: ListView gave
each item a tight cross-axis constraint equal to the rail's
current width (48 collapsed, 196 expanded). Inside the item,
the AnimatedContainer filled that constraint and laid out a
mainAxisSize.min Row at the start of the AC's content area.
The icon's pixel-X depended on the AC's padding _and_ the
constraint; even with equal padding the rendered layout
shifted across the expand animation because Flutter
re-resolved alignment under a moving constraint.
Replace the entire pattern with a fixed-width icon column
that doesn't care about the parent constraint:
ListView (padding 0) → Item Row
SizedBox(width: 72) → Center → icon ← anchored to literal x
if expanded → Expanded → label text ← grows into remaining space
Same pattern for the brand/connection/channel rows above. The
72 px matches the rail's collapsed width exactly, so collapsed
rail = one icon column + nothing; expanded rail = same icon
column + label slot. Icon's pixel-X is now provably constant.
Removes the AC's horizontal padding entirely — the icon
column owns the left anchor. The background highlight still
spans the full row width (typical nav-rail UX), and the rail
expand animation only moves the rail's right edge, never any
icon-bearing element.
Also passes an `iconColumnWidth: 72` to _SidebarItem instead
of hard-coding the magic number in two places.
Version 0.51.3 → 0.51.4.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Two Stefan-reported fixes after the v0.51.x live test:
1. Sidebar icons appeared to shift between collapsed and
expanded. Mathematically the icons stayed at x=24 (
ListView padding + AC padding), but the Column's default
crossAxisAlignment.center re-centered the brand mark
AND the top indicators (version, connection, channel) on
every expand → in a 72 px rail the brand sits at x≈36,
in a 220 px rail it sits at x≈110. Stefan perceived this
horizontal repositioning of the brand+pills as a shift
of the icons below.
Fix: Column.crossAxisAlignment = start + left-pad every
top item (brand mark, "F∆I Studio" label, version,
connection pill, channel pill, mini-version, connection
dot, channel chip) by exactly the same offset the
destinations use (ListView.padding + AC.padding =
FaiSpace.md + FaiSpace.md). The whole rail now reads as
one stable left edge through the expand animation.
2. Pull fai_studio_flow_editor 0.1.2 — fixes the
"opening hello.yaml shows extract-with-approval content"
bug by switching `_code.text = ...` to the canonical
`_code.fullText = ...` setter in the package.
Version: 0.51.2 → 0.51.3
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Updates fai_studio_flow_editor pin from 51f9a1d to 8b918f8.
Upstream fix: the editor's dirty-detection compared
_code.text against a snapshot taken before CodeController
normalisation, so opening any flow whose loaded text the
controller silently rewrote (trailing-newline handling, YAML
fold markers, line-ending normalisation) immediately read as
dirty. Clicking a different file in the list then raised the
"Discard unsaved changes?" prompt even though the operator
hadn't typed anything.
The package now compares _code.fullText (the canonical
post-processed value) against a baseline captured AFTER the
text assignment. Comparing fullText-to-fullText eliminates
the spurious diff.
Version: 0.51.1 → 0.51.2
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Two UX bugs Stefan flagged on the v0.51.0 collapsible rail:
1. Icons re-positioned when the rail expanded. Operators
already pointing at an icon had to chase it as the rail
grew. Two causes: the _SidebarItem switched between
`Center(icon)` (collapsed) and `Row(icon, gap, label)`
(expanded), and the AnimatedContainer's horizontal padding
switched between FaiSpace.sm (collapsed) and FaiSpace.md
(expanded). Both flipped the icon's effective X-position.
Now: same Row, same AnimatedContainer padding, in both
states. The icon sits at the same X-pixel always; only
the label slot toggles open beside it.
2. Top of the collapsed rail was wasted (only the brand-mark
was visible; version + connection + channel disappeared
completely). Use the same vertical real estate for
glance-able compact equivalents:
* tiny "v0.51" version text in mono below the brand
* _CollapsedConnectionDot — 10 px circle, green/red/amber
tonality matching FaiDeltaMark's mode states. Tooltip
surfaces the full endpoint string.
* _CollapsedChannelChip — 18 px circle with the channel's
first letter (P / B / D / L) in the same accent tone
the full pill uses, so "production" stays unmistakably
red even at-a-glance.
When the rail expands, the full pills replace the compact
indicators as before. Tests still green (the existing widget
test booted without hover-expand and that path stays correct
either way).
Version: 0.51.0 → 0.51.1
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The flow editor was internal to Studio (lib/pages/flow_editor.dart).
Per Stefan's review feedback ("austauschbar wäre schöner"),
extract it into its own Forgejo repo so the host can swap
the implementation without touching Studio.
New repo: https://git.flemming.ai/fai/studio-flow-editor
Studio's pubspec.yaml now references the package by git URL:
dependencies:
fai_studio_flow_editor:
git:
url: https://git.flemming.ai/fai/studio-flow-editor
ref: main
To swap the editor:
1. Fork (or write a new) fai/studio-flow-editor.
2. Keep the FlowEditorPage(initialFlowName, locale, onRun)
constructor signature — the stable host contract.
3. Point the pubspec at your fork.
4. Rebuild Studio.
Adapter pattern: _FlowEditorAdapter in main.dart resolves the
package's runtime dependencies (locale via Localizations,
onRun via HubService) from the BuildContext, then constructs
the package's FlowEditorPage. Same pattern in flows.dart for
the pencil → editor route push, so a future operator-side
locale switch propagates correctly.
The package brings its own copies of FaiSpace tokens, minimal
FaiEmptyState/FaiErrorBox widgets, and an inline EN+DE l10n
table — accepting a small amount of visual drift in exchange
for true package independence. flutter_code_editor +
highlight move from Studio's pubspec to the package's.
Deleted:
lib/pages/flow_editor.dart → package's lib/src/flow_editor_page.dart
test/flow_editor_test.dart → package's test/ (next commit there)
Bumped:
pubspec.yaml version 0.50.0 → 0.51.0
main.dart kStudioVersion 0.50.0 → 0.51.0
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Three operator-visible improvements per Stefan's review of
the v0.50.0 editor.
1. Pencil icon on Flows page now opens Studio's own flow
editor (route push) preloaded with the selected flow,
replacing the previous "open in OS default editor"
(SystemActions.openInOs) behaviour. The editor's AppBar
gains a back arrow when reached via route push; reaching
the editor via the nav rail leaves it bare. Tooltip
string updated in EN + DE.
2. New FlowEditorPage `initialFlowName` parameter. When set,
the page loads that flow on first frame (via post-frame
callback so the BuildContext is mounted before
_openByName runs). When null (the nav-rail path), the
editor opens to its empty state as before.
3. Sidebar (_Sidebar) is now collapsed-by-default: shows
just icons in a 72px-wide rail with per-destination
tooltips. Hovering the rail expands it to 220px (the
old width) with brand-mark + labels + the full footer
row (theme toggle, language toggle, clock, settings).
Collapsed footer shows the settings icon only. Brand-
mark (FaiDeltaMark) stays visible in both states so the
live/idle status dot is always glanceable.
Side-effect: SystemActions import in flows.dart is no
longer needed (the pencil no longer shells out) — removed.
widget_test.dart: dropped the per-destination Text-presence
asserts since labels are now Tooltips when collapsed.
Hover-expand testing triggers RenderFlex-overflow mid-
animation in widget tests (the AnimatedContainer's width
transitions through a constraint slimmer than the Row's
intrinsic min). The booting-without-throwing assertion
remains; per-destination presence stays in the per-page
test suites.
Both arb files updated with the new strings (navFlowEditor
already existed; added flowEditorBackTooltip + retouched
flowsOpenInEditorTooltip).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Previously a failed runSavedFlow dumped the raw exception
into FaiErrorBox in the right panel. Hub gRPC failures
(network, missing module, schema reject, …) are now mapped
through friendlyError so the operator sees a one-sentence
headline + optional recovery hint, with the verbatim error
still accessible behind FaiErrorBox's expand-on-tap.
Matches the same pattern already used by every other Studio
page that surfaces a hub error.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Wraps the FlowEditorPage in Shortcuts/Actions so
Cmd+S (or Ctrl+S) saves and Cmd+Enter (or Ctrl+Enter) runs
the active flow, no matter whether the code field or
toolbar has focus. Matches the rest of Studio's keyboard-
first ethos.
Disabled when no flow is open or an operation is already
in progress.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
New top-level destination "Editor" (Cmd+5) ships as Studio's
fifth surface. The editor reads + writes flow YAML directly
under ~/.fai/data/flows/ via dart:io — the hub picks up the
changes on the next listFlows / runSavedFlow call.
Layout: two-pane shell. Left (240 px) is the file list; right
flexes to the code pane and an optional results column when
Run produces output. Top toolbar exposes:
* filename + dirty-mark
* New flow (scaffolds from a debug.echo template)
* Save (writes the active file to disk)
* Run (saves first if dirty, calls
HubService.runSavedFlow, surfaces typed FlowOutputs in
a side panel)
* Refresh
YAML highlighting via flutter_code_editor + the highlight
package's yaml language. Lightweight style map mapping the
five token classes that actually appear in flow YAML
(attr / string / number / comment / subst for the
${{ ... }} template syntax) to FaiTheme colors — keeps the
editor visually consistent with the rest of Studio.
New-flow naming uses a FilteringTextInputFormatter that
restricts the name to [a-z0-9_-]. A "name already exists"
SnackBar surfaces the conflict instead of silently
overwriting.
Bilingual strings shipped (en.arb + de.arb) for every
operator-facing string: toolbar buttons, dialogs, empty
states, file-exists error, run-output header.
New deps:
* flutter_code_editor ^0.3.5
* highlight (transitive — pinned as direct so the
yaml-language import has its declared dependency).
Smoke-test (test/flow_editor_test.dart) pumps the page and
asserts the empty-state + toolbar render without throwing
on hosts that don't have ~/.fai/data/flows yet. The full
file-list + open-on-tap flow needs a writable HOME override
which dart:io's read-only Platform.environment doesn't allow
inside a test isolate — that path lives in the integration
suite as a follow-up.
Version bumps:
* pubspec.yaml: 0.49.1 → 0.50.0
* main.dart kStudioVersion: 0.42.0 → 0.50.0 (had drifted
behind pubspec; brought back into sync as part of this
bump)
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The doc-reader bottom-sheet in welcome.dart hits rootBundle
to load assets/docs/<slug>[_de].md. When the asset isn't in
the bundle (e.g. the binary on disk predates the
flutter_markdown_plus migration in 69b54629 or an in-progress
asset reorganisation), the FutureBuilder's error branch dumps
the raw PlatformException text into FaiErrorBox with no
hint at what the operator should do.
Switch the throw to a structured `_DocReaderError` that
carries:
- the slug, locale, and both attempted asset paths
- the underlying error message
- a precomputed `forgejoUrl` pointing at the same doc on
the platform repo's main branch
The error UI gains a TextButton.icon below FaiErrorBox that
opens the Forgejo copy in the OS browser via the existing
SystemActions.openInOs path — operators always have a
working out, even when the local bundle is broken.
The error toString() also surfaces "rebuild Studio" as the
top suggestion, since the most common cause for asset
mismatches is a stale binary running against a newer source
tree.
New regression test (test/load_docs_test.dart) pumps the
exact code path the sheet uses (rootBundle.loadString +
Markdown with FaiTheme.markdownStyle) against all eight asset
paths (architecture/security/audit/flows × en/de). Catches a
future bundle drift before an operator notices.
No behavioural change on the happy path. Both the rendered
markdown widget and the closed-sheet animation are unchanged.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
`hub_auth_token.dart` write() previously did
await f.writeAsString(token); // file is now 0644 (umask)
await Process.run('chmod', ['600', f.path]);
which leaves a TOCTOU window where the file is world-readable
between the writeAsString and the chmod call. On a multi-user
host another user could read the bearer token during that
window.
New flow:
await tmp.writeAsString(token); // .tmp file
await chmod(600, tmp.path); // restrict mode FIRST
await tmp.rename(f.path); // atomic POSIX rename
The destination is never visible with permissive perms. Also
sets `~/.fai/` itself to 0700 on Unix on first creation so
other users on a shared host can't enumerate the directory.
dart analyze clean; flutter test green (12 tests).
Bumped pubspec to 0.49.1.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Lifts every direct dependency Studio carries to its latest
available version:
fai_client_sdk: 0.16.0 → 0.17.0 (grpc 5, protobuf 6, new
bindings)
+ the SDK major dragged dbus 0.7.13, xml 7.0.1, analyzer
13.0.0, _fe_analyzer_shared 100.0.0 via the constraint
solver in the platform→sdk→studio chain.
What Studio's call sites needed in code: nothing. `HubClient`
already wraps every gRPC + protobuf type, so the major bumps
land transparently. The integration tests against the live
hub fixture pass without changes (11/11 green).
Remaining outdated transitives are all Flutter SDK 3.44.0
pins:
- meta 1.18.0 → 1.18.2
- vector_math 2.2.0 → 2.3.0
- win32 5.15.0 → 6.3.0
These lift with the next Flutter stable bump. No action
possible from Studio's pubspec today.
Bumped pubspec to 0.49.0.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
`flutter pub upgrade` lifts every transitive dep that the
constraint solver can reach: google_cloud 0.4.1→0.5.0,
googleapis_auth 2.3.0→2.3.1, hooks 1.0.3→2.0.0, objective_c
9.3.0→9.4.1 (plus xml 6.6.1).
Direct: file_picker 8.0.0 → 11.0.0. The major jump is the
move from instance API (`FilePicker.platform.pickFiles(...)`)
to static API (`FilePicker.pickFiles(...)`). Two call sites
updated (`lib/pages/flows.dart` for the run-flow form's
file input, `lib/widgets/fai_flow_output.dart` for the
flow-output "Save as" affordance) — both functionally
identical, just the surface that lost the `.platform.`
hop.
Locked behind external pins (no action possible from
Studio side this pass):
- grpc 4.2.0 → 5.1.0 ─┐
- protobuf 4.2.0 → 6.0.0 ┤── pinned by fai_client_sdk; a
│ major SDK bump is its own
│ piece of work (regenerated
│ bindings, possibly Studio
│ call-site shifts).
- characters / meta / vector_math / win32 / native_toolchain_c
─ all pinned by the Flutter SDK; lift with the next
Flutter SDK upgrade.
dart analyze clean; flutter test green (12 tests pass).
Bumped pubspec to 0.48.1.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Two operator surfaces shipped together:
**Multi-version uninstall picker** — when more than one version
of a `(provider, name)` is installed side-by-side, both the
module-sheet "Uninstall" affordance and the store-detail
"Uninstall" affordance now ask the operator which version to
remove before calling the RPC. The hub's wire-level support for
this (UninstallModuleRequest.version) was already there; Studio
just wasn't using it. Picker pre-selects the highest version so
single-version flows still take one click.
- `HubService.installedVersions(name)` enumerates the installed
versions via the capabilities list.
- `HubService.uninstallModule(name, version: ...)` forwards
the version into the RPC.
- `_UninstallVersionPickerDialog` (module sheet) and
`_StoreUninstallVersionPickerDialog` (store) host the
picker — separate widgets so each surface can evolve copy
independently. Uses `RadioGroup<String>` for Flutter
3.32+ deprecation compliance.
**Default scope editor** — new DEFAULT SCOPE panel in Settings
that calls the freshly-added HubAdmin RPCs
`GetDefaultScope` / `SetDefaultScope`. Operators can:
- reorder publisher segments with up/down buttons
(first match wins in the bare-form resolver),
- delete entries (hub still rejects empty list — Studio
surfaces the constraint inline),
- add arbitrary entries via the text field,
- add catalog-known publishers via suggestion chips
(sorted alphabetically, populated from the catalog).
Every change persists to `~/.fai/config.yaml` via the hub
and hot-swaps the in-memory copy without a daemon restart.
Bumped pubspec to 0.48.0. dart analyze clean (No issues
found!); flutter test green (11 tests).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Store cards + detail-sheet now prefix the version pill with
`~` and attach a tooltip for federated entries (MCP / n8n /
Temporal). Upstream services don't honour semver, so the
version pill is a label, not a contract — the tilde says
"this number may change without a version bump."
Three render sites updated to stay visually consistent:
- compact card pill in grid view
- expanded card pill row
- detail-sheet header pill cluster
EN + DE l10n entries added (`storeAdvisoryVersionTooltip`)
and `app_localizations.dart` regenerated.
dart analyze clean; flutter test green (11 tests).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Studio gains a new "HUB AUTHENTICATION" panel in Settings
mirroring the existing registry-credentials panel:
- `lib/data/hub_auth_token.dart` — `~/.fai/hub-auth-token`
helper (read / write / clear, mode 0600 on Unix). Sister
of `RegistryToken` with the same on-disk hygiene.
- `HubService.loadPersistedEndpoint` now reads the token at
startup and reconnects with it. `reconnect()` grew an
`authToken:` parameter with a sentinel that distinguishes
"keep current" from "drop". `reloadAuthToken()` is the
one-liner Settings calls after save / clear.
- `_HubAuthTokenPanel` in `fai_settings_dialog.dart` — paste
with show/hide toggle, save button, clear button, status
pill ("Configured (40 chars)" / "Not set (anonymous)"),
storage-location hint. Trimmed token length only — the
secret never round-trips back into the UI after save.
- EN + DE ARB entries (`hubAuthToken*`) + regenerated
`app_localizations.dart` keep the bilingual surface
consistent.
The hub side (auth.tokens config + tower middleware) shipped
on the platform side 2026-05-28 (43a54a2). Until now an
operator had no GUI path to consume it: they had to find the
file in their home dir, paste the token by hand, and bounce
Studio. This panel closes that loop.
Bumped pubspec to 0.47.0. dart analyze clean (No issues
found!); flutter test green (11 tests pass).
Block E item 1 of 4 done. Capability-picker badges,
default_scope editor, multi-version uninstall picker follow.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Studio's Doctor page _ModulesPanel now shows the source
breakdown directly under the module/capability count:
Modules: 6 loaded, 10 capabilities
6 bundle · 3 mcp · 1 system
Reads `CapabilityEntry.sourceKind` (Phase D wire field).
Pre-0.12 hubs return empty sourceKind; the breakdown panel
falls back to grouping by the legacy `kind` string so older
deployments still see a useful count.
Two data-layer additions:
- `DoctorSnapshot.capabilitiesBySource: Map<String, int>`
- `_sourceBreakdown(Map<String, int>) -> String` renders
the line in deterministic alphabetical order.
dart analyze + flutter test green (11 integration tests).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Adds the missing UI surface for SPARK / Temporal federated
capabilities. Previously only mcp and n8n showed provenance
pills; temporal-sourced entries fell through to the default
"no badge" case.
- storeSourceTemporal + storeProvenanceTemporal l10n keys
(en + de). Pill format: "temporal · <provider>", icon
Icons.timer_outlined, accent tone.
- Source filter chip row gains a "Temporal" option in the
Store filter dialog.
- `_StoreProvenancePill` adds a `case 'temporal'` branch.
Operators federating a SPARK cluster (`services: -
kind: temporal`) now see their workflows visually labelled
as Temporal-sourced in the Store grid + filter UI.
dart analyze green. No test changes required — the
provenance pill is render-only.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Studio's `_sourceForItem` now prefers the wire-level
`sourceKind` field (populated by 0.12+ hubs) over the legacy
name-prefix heuristic. Adds a "temporal" bucket alongside
"mcp" / "n8n" / "native" / "federated" so SPARK-federated
workflows render with their own transport pill on the store
grid + filter chip.
Pre-0.12 hubs return empty `sourceKind` and the legacy
sniffing path still works -- back-compat preserved.
`StoreItem` data class accepts an optional `sourceKind`
parameter (defaults to empty). dart analyze green.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Studio's UI-side `CapabilityInfo` record now carries the
three fields that the F∆I 0.12.0 hub exposes on
CapabilityEntry:
- `provider` — publisher identity ("fai", "fai.system",
"bmds.spark", ...)
- `sourceKind` — transport ("bundle", "system", "mcp",
"n8n", "temporal", "webhook")
- `versionAdvisory` — true for federation sources whose
version pin is a label, not a contract
`allCapabilities()` populates them from the freshly-regenerated
protobuf bindings. Defaults are backward-compatible (empty
strings + false) so a pre-0.12 hub still works against this
Studio build.
Badge rendering in the capability picker / store grid lands
in a follow-up Studio commit; this change is the data-layer
plumbing only. dart analyze green.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The Dart SDK package was renamed package: fai_dart_sdk →
fai_client_sdk and the dir + Forgejo repo got a -dart language
suffix per the three SDK families convention in
fai/platform/docs/architecture/sdks.md. Updates the path
dep, dep name, every `package:fai_dart_sdk/...` import, and
the one comment that named the SDK.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Studio plugins moved to a dedicated fai_plugins/ workspace
directory; integration test path updated to match.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Two pieces of the Studio plugin host:
- `data/theme_plugin.dart`: discovery, persistence, ColorScheme
translation. Plugins listed via list_capabilities filtered
to studio.theme.*; selection persisted in SharedPreferences;
loadThemePluginSchemes builds a ColorScheme pair from the
plugin's 14 ARGB tokens per brightness.
- `main.dart`: StudioApp gains themePluginNotifier +
setThemePlugin(). MaterialApp wraps a FutureBuilder that
re-fetches the plugin's ColorSchemes whenever the
capability flips; falls back to FaiTheme.light/dark on
null or load failure.
- `fai_settings_dialog.dart`: new `_ThemePluginPanel` shows a
dropdown of installed studio.theme.* capabilities + a
"Built-in" entry. Switching applies immediately.
- HubService.invokePluginTranslate added as the typed wrapper
for the new gRPC RPC — ready for the FaiEnBadge swap-out
in the next iteration.
- 4 new l10n keys (themePluginHeader / None / Hint / Empty),
EN+DE.
flutter analyze + flutter test (widget + friendly_error): both
green. The integration test for the theme picker is the
existing fai_runtime plugin_theme + Studio-side
invokePluginTheme path that round-trips through the new
mirror-installable studio-theme-solarized bundle.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Wraps the new InvokePluginTheme RPC and ships an end-to-end
test that loads studio-theme-solarized into a fresh
HubFixture and asserts the light/dark schemes round-trip.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Moves the list of private organisation / pilot / codename
strings the security gate blocks OUT of the repo entirely.
Before: tools/security/check-staged.sh + the Today AI prompt
+ docs/today-pipeline.md held the strings in plaintext. The
whole point of the gate is to keep certain strings out of
committed artefacts, so holding them in a committed
artefact was self-defeating — anyone with read access to
the repo trivially recovered the very list we tried to
protect.
After:
- The gate reads a runtime file
`${FAI_BANNED_TERMS_FILE:-~/.fai-security/banned-terms.txt}`
at scan time. One regex per line, `#`-prefixed comments,
matched case-insensitively against staged diffs and
commit messages. Repos contain no copy.
- Pre-commit / commit-msg modes log a warning + skip the
confidential-terms scan if the file is missing (fresh
checkouts shouldn't trip until the operator bootstraps
the list).
- CI mode (`check-staged.sh ci`) FAILS when the file is
missing — runners are expected to be bootstrapped by
their deploy step.
- The unit-test harness uses a synthetic placeholder term
(`SYNTHETIC_BANNED_TERM_XYZZY`) injected via a temp
banned-terms file, so the test never references real
customer names.
- docs/today-pipeline.md + tools/today/prompt.template.md
point at the runtime file instead of enumerating terms.
Operator bootstrap (one-time, per machine):
mkdir -p ~/.fai-security && chmod 700 ~/.fai-security
printf '\\b%s\\b\\n' TERM_A TERM_B > ~/.fai-security/banned-terms.txt
chmod 600 ~/.fai-security/banned-terms.txt
Gate self-tests: 18 passed, 0 failed.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Two clean-ups:
- `fai_system_ai_editor.dart` uses the new `RadioGroup<T>`
ancestor pattern Material 3 introduced after Flutter 3.32.
Same UX (three radios for off/redacted/full), no
deprecation warnings, `flutter analyze` is now zero-issue
for the whole project.
- New `install_install_planned_test.dart` asserts that
installing a `planned` seed entry surfaces a clear,
human-readable error class (no panic, no silent success).
Locks in the failed-precondition path Studio's
friendly-error mapper depends on.
Integration test suite is now 3 scenarios:
- capabilities_test.dart: system.approval+kind tag
- install_install_planned_test.dart: planned-install error
All run against a fresh `fai serve` subprocess via
HubFixture, ~2 s total.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Two follow-ups to the May-2026 trust pass.
flutter_markdown_plus migration:
- pubspec swaps `flutter_markdown ^0.7.7` (discontinued
upstream) for `flutter_markdown_plus ^1.0.3`, the
actively-maintained fork. API surface
(MarkdownStyleSheet, Markdown, MarkdownBody) is
unchanged — the four import sites in welcome, store,
flow_output, and theme.dart get an updated package
string and that's it.
- All Studio analyzer + unit-test suites stay green.
Integration test scaffold:
- New `test/integration/hub_fixture.dart` boots a real
`fai serve` subprocess on a free port against a temp
FAI_DATA_DIR, polls until Healthy, exposes a ready
HubClient. Idempotent teardown wipes the temp dir.
- Resolves the `fai` binary from PATH first, then from
`../fai_platform/target/release/fai`. When neither
exists, the fixture calls `markTestSkipped` with a
clear message — fresh checkouts don't fail.
- One canonical test in `capabilities_test.dart` asserts
on the bug class the May trust pass surfaced: that
`system.approval` appears in `list_capabilities` with
`kind=builtin` so Studio's missing-deps check never
tries to install it. Plus a contract-shape test that
every cap's `kind` is one of the three known wire
values.
- README documents the cold-start gotcha (first
`fai serve` per machine takes ~30s to build the
curated-model DB) plus the manual warmup recipe.
Not in CI yet — wiring needs the platform build job to
publish `fai` as a CI artifact for downstream consumption.
Deferred until enough integration tests exist to justify
the CI minutes.
Signed-off-by: flemming-it <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
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>
flutter_markdown's MarkdownStyleSheet.fromTheme defaults the
blockquote background to a hardcoded Colors.blue.shade100. In
Dark Theme the text colour stays white-on-surface, so a
blockquote rendered '> Note: ...' shows as white text on
light blue — unreadable.
Override blockquoteDecoration to use theme tokens
(surfaceContainer + left primary accent border) and pin
blockquote text to onSurface. tableCellsDecoration gets the
same theme-driven treatment for the same class of issue.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Three Stefan-found bugs in one pass:
1. Documentation rendered in English even when Studio was set
to German. _loadDocs now passes Localizations.localeOf(...)
to fetchModuleDocs(); the hub tries README.de.md before
README.md when locale='de'.
2. 'Keine Treffer' flickered on every keystroke. _onAskTyping
used to call _runSearch synchronously per character — a
short 2-3-letter substring matched nothing, so the empty-
state showed for a frame, then the next character revealed
results. Add a 250ms typing debounce (Timer + cancel on
dispose) so the search only re-runs once typing pauses.
3. Submit button did substring search even when System-AI was
enabled — the _looksLikeQuestion heuristic only routed
4+-word / question-mark queries through the LLM. The user
reasonably expected an explicit submit (with the AI-icon
showing on the button) to actually use AI. New behaviour:
when system-AI is enabled, the submit button always calls
_runAiQuery; live-typing keeps substring filtering. The
heuristic helper was removed (now unused).
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The 'Documentation' row in the module-detail sheet showed
a RenderFlex horizontal overflow when the sheet is narrow
(~340dp on mobile/portrait). The Row had:
[Button] [Spacer] [Text(hint)]
without any Expanded/Flexible wrapper, so the hint text could
not break and pushed past the right edge.
Fix: wrap the hint Text in Expanded. Text now wraps to a
second line at narrow widths instead of overflowing. Also
explicitly set crossAxisAlignment.center so the wrapped text
visually aligns with the button on a single baseline.
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>
18 scenarios that verify the security script's rules still
trip on every tripwire — PATs, AWS keys, PEM headers, env
files, allowlist exempts, confidentiality terms, marketing
phrases, .security-allow exclusion, plus message-mode
(Conventional Commits, DCO, Claude trailer, banned phrases).
Wired into the security workflow as a second-line gate after
the diff-based check. Catches the refactor-weakens-a-rule
class of regression: the diff scan can be green while a rule
silently no-ops; the harness fails loudly when that happens.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The hub's v0.10.91 install error message already tells the
operator exactly what to do — set FAI_REGISTRY_TOKEN or save
to ~/.fai/registry-token via Settings. But that text lives in
a FaiErrorBox inside the install dialog. Reading the buried
recovery instruction, finding Settings, scrolling to the
Registry credentials panel, pasting, then re-clicking the
install pill is five hops for what should be one.
When at least one item in the install dialog fails with the
"registry returned an HTML page" / "no registry token
configured" signature, the dialog now surfaces a prominent
filled "Add registry token" button next to Close. Click closes
the install dialog and opens the Settings dialog directly so
the operator pastes + saves without losing context. After
Settings dismisses, the operator is back at the flow card and
re-clicks the same pill to retry.
Detection is signature-based (substring match on the hub's
error text) so it activates whether the install was a single-
pill click or an "Install all" sequence. Other failure modes
(network down, signature invalid, sha mismatch) still show
just the Close button — the CTA is specific to the auth-wall
case it can actually unblock.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
UI parity with the platform CLI: `fai reset` (v0.10.93) is now
reachable from Settings. The panel shows the reset blurb plus
two checkboxes mirroring the CLI flags (Keep modules / Keep
audit log + saved flows), then a destructive-styled "Reset hub
state" button. Click goes through a confirm dialog before
SystemActions.faiReset is invoked.
After the CLI returns, Studio bounces its gRPC channel against
the same endpoint (the daemon restarts on the same channel +
port, so no Settings change needed) and reloads every panel:
channel status, system AI, MCP clients, n8n endpoints, registry
token. Output (success or failure) is shown in a copyable error
box so any unexpected stderr lands somewhere the operator can
paste from.
Sits at the bottom of the dialog under "HUB MAINTENANCE",
separated from the everyday config so a destructive button
doesn't crowd the day-to-day controls.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Runs `tools/security/check-staged.sh ci` on every push to main
and every PR. Same script as the local pre-commit hook so the gate
is identical whether or not a contributor activated the local
hook via `bash tools/install-hooks.sh`. No Flutter / Dart toolchain
needed in the runner — the check is plain bash + git.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
The hub install path now reads `~/.fai/registry-token` as a
fallback when `FAI_REGISTRY_TOKEN` is unset (platform v0.10.92).
Studio now writes that file directly: a fresh operator pastes
the PAT into Settings → Registry credentials, hits Save, and
the next install attempt resolves the auth wall without any
shell or env-var setup.
The token never round-trips back into Studio after save —
status is shown only as "Configured (40 chars)" / "Not set"
with no display of the secret itself. The Clear action deletes
the file. On Unix the file is chmod-ed to 0600 (owner-only
read/write); Windows leaves the default user ACL in place.
Storage is strictly local: `~/.fai/registry-token`, never sent
to a remote service. The hint text under the field says so to
make the data flow explicit.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
Mirrors the hook + script from `fai/platform`@1ebf893 verbatim
so commits to fai_studio go through the same gate: secrets,
forbidden filenames, confidentiality references, marketing-
speak, DCO sign-off, Conventional Commits subject, no Claude
co-author trailer.
A `.security-allow` file at the repo root extends the script's
universal excludes with three Studio-specific paths whose
content legitimately includes the filtered terms — the
Today-Hero proposal pipeline (LLM prompt + accept gate), its
runtime loader equivalent, and its operator-facing policy doc.
No CI mirror yet — Studio doesn't have a Forgejo workflow
(Flutter-on-DinD is a chunk of work). The local hook is the
gate for now; CI mirror follows when Flutter CI lands.
Activate once with `bash tools/install-hooks.sh`.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
When a saved flow is missing modules, each capability pill in the
"Needs:" row is now clickable: tap one and a per-item progress
dialog walks the bare capability name through installModule, then
the flows page refreshes. When more than one capability is missing,
an extra "Install all (N)" button runs the same dialog over the
whole list sequentially so a fresh operator can take an unrunnable
flow and one click later have its dependencies resolved.
Also adds a pencil icon next to Run that hands the YAML path to
the OS via SystemActions.openInOs — the operator's default editor
for .yaml decides what opens. Makes the path next to the flow name
actionable instead of decorative.
The install dialog renders one row per spec with a status icon
(pending circle / spinner / check / error), shows the installed
version on success, and surfaces the full error in a copyable
FaiErrorBox on failure so the operator can paste it back.
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>
The free-form key=value run-flow dialog produced two
unhelpful failures every time an operator hit Run:
1. Submit empty → "step references missing value
'\$inputs.document'" from the hub. Studio sent a zero-
entry inputs map because nothing told the operator the
flow declared a required input.
2. Type `document=@/path/to/file` → "path not found".
Dart's `File()` doesn't expand `~`, and on macOS
sandboxed Studio can't read arbitrary paths anyway.
Both failure modes are gone. The dialog is now a typed form:
- On open, fetches `getFlowDefinition(name)` (new SDK
v0.15.0 wrapper around the v0.10.89 hub RPC). While that
resolves, a small spinner shows
"Loading inputs…"; on failure, an inline `FaiErrorBox`
with the exact RPC error and a copy button replaces the
spinner.
- Renders one form-field per declared input. The flow
YAML's verbatim type tag drives the widget choice:
`bytes` / `file` → "Choose file…" button + picked-file
readout, plain TextField for everything else. The type
tag is shown next to the input name as a pill so a flow
author who picks a less-common type still gets a hint.
- Bytes inputs route through `file_picker` with
`withData: true`, which means the OS file dialog handles
read access — sandboxed Studio gets the bytes inline
rather than a path it can't open. Falls back to
`File(path).readAsBytes()` on Linux configs that don't
honour `withData` for large files; failures show a
copyable SnackBar.
- The Run button enables only when every declared input has
a value (text non-empty / file picked). Empty submission
is impossible; the cryptic hub-side
"missing value" error stops surfacing.
- The run-result dialog's error path uses `FaiErrorBox`
instead of the previous plain `SelectableText`, so any
hub-side flow error (permission denied, module crash,
whatever) is one click to clipboard.
11 new ARB keys cover the form's labels, the file-picker
states, the loading / failed-definition messages, the
"This flow declares no inputs" empty state, and the
run-error dialog title.
`file_picker: ^8.0.0` added to pubspec.yaml.
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>
Three concrete changes against operator feedback that the
Today carousel was visually jumping when arrowed and
genre-mixing platform-architecture education with store-
actionable highlights.
- Carousel size pinned. The hero's outer Container picks up
`BoxConstraints(minHeight: 240)` so a slide with one
paragraph and a slide with three render at the same
height. Prev/next no longer reflows the rest of the page.
- Today fallback stories trimmed and re-themed. The four
shipped slides drop to three:
- "Public sources" (DeepWiki / Semgrep one-click) — kept
- "Three text modules already in the store" — new,
points the operator at text.extract / text.summarize /
text.translate in the grid below
- "Try the extract → summarize flow" — new, points at
flows/extract-summarize.yaml
Architecture-education stories (sandbox model, hash-
chained audit, air-gap posture) are gone from this surface
— they belong on the Welcome page that
`docs/landing-page-design.md` lays out.
- DeepWiki + Semgrep added to the Settings → MCP-Clients
add-server suggestion-chip catalogue. Until now the chips
were nine stdio servers that need Node + npx; the two
HTTPS public sources only existed as one-click cards in
the Today hero. Operators who dismissed the hero had no
in-Settings path to find them. The new entries sit at the
top of the catalogue with an explicit "Public HTTPS — no
Node, no API key" descriptor and the same icons the Today
hero already uses.
- `docs/landing-page-design.md` (new). Captures the design
for a sidebar Welcome page that hosts the three-pillar
intro, the trust-posture deck, the getting-started
checklist, and an embedded-doc reader so operator-facing
documentation stays inside Studio (`flutter_markdown`
rendering of bundled `assets/docs/*.md`) instead of
clicking out to a browser. Three-phase implementation
plan: scaffolding, embedded docs, computed checklist.
Build is gated on operator alignment; this doc is the
alignment artefact.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>