From 3b07d340d5770ba254b05d4190d859e27e867c2f Mon Sep 17 00:00:00 2001 From: flemming-it Date: Sat, 9 May 2026 00:24:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(studio):=20Welcome=20page=20Phase=20B=20?= =?UTF-8?q?=E2=80=94=20embedded=20doc=20reader=20(v0.38.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/_.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 --- assets/docs/architecture.md | 51 +++++ assets/docs/architecture_de.md | 55 ++++++ assets/docs/audit.md | 69 +++++++ assets/docs/audit_de.md | 70 +++++++ assets/docs/flows.md | 90 +++++++++ assets/docs/flows_de.md | 92 +++++++++ assets/docs/security.md | 66 +++++++ assets/docs/security_de.md | 68 +++++++ lib/l10n/app_de.arb | 13 ++ lib/l10n/app_en.arb | 13 ++ lib/l10n/app_localizations.dart | 72 +++++++ lib/l10n/app_localizations_de.dart | 43 +++++ lib/l10n/app_localizations_en.dart | 43 +++++ lib/main.dart | 2 +- lib/pages/welcome.dart | 299 ++++++++++++++++++++++++++++- pubspec.yaml | 8 +- 16 files changed, 1051 insertions(+), 3 deletions(-) create mode 100644 assets/docs/architecture.md create mode 100644 assets/docs/architecture_de.md create mode 100644 assets/docs/audit.md create mode 100644 assets/docs/audit_de.md create mode 100644 assets/docs/flows.md create mode 100644 assets/docs/flows_de.md create mode 100644 assets/docs/security.md create mode 100644 assets/docs/security_de.md diff --git a/assets/docs/architecture.md b/assets/docs/architecture.md new file mode 100644 index 0000000..998fe27 --- /dev/null +++ b/assets/docs/architecture.md @@ -0,0 +1,51 @@ +# Architecture in one paragraph + +F∆I has three concepts you'll meet everywhere: **Hub**, +**Module**, **Flow**. + +**The hub** is a single Rust binary called `fai`. It runs as a +gRPC server, loads modules, executes flows, and writes events +to a SQLite database. One binary, no Docker, no message broker, +no cluster. You can run it on a laptop, a Raspberry Pi, or a +locked-down KRITIS server — same artefact every time. + +**A module** is a small WebAssembly component that does one +thing well. Modules are sandboxed: each one declares the +network endpoints, files, and environment variables it needs, +and the hub enforces that list. A module that doesn't ask for +network access can't reach the network. Modules ship as `.fai` +bundles with a CycloneDX SBOM and a sigstore signature. + +**A flow** is a YAML file with three top-level keys: +`inputs:`, `steps:`, `outputs:`. Steps run in declared order, +each step calls one module, and outputs from one step can feed +inputs of the next. Flows are deterministic — same inputs +produce the same audit trail. + +## How the pieces fit + +``` +Studio ──gRPC──▶ Hub ──loads──▶ Module (WASM) + │ + ├──executes──▶ Flow (YAML chain of modules) + │ + └──writes────▶ Audit log (hash-chained, in + SQLite; WORM-1 by construction) +``` + +## What you don't need + +- No container runtime. The hub doesn't shell out to Docker + or Podman. Modules are WASM components loaded in-process. +- No external database. SQLite ships with the hub. +- No background services. One process; restart with + `fai daemon restart` — see Doctor → Daemon control. + +## Where to go next in this app + +- **Store** to discover modules and federated MCP / n8n + capabilities, install them in one click. +- **Flows** to run the sample flows that ship with the hub + (e.g. `extract-summarize`). +- **Audit** to see every event the hub recorded — the + hash-chained log is the platform's source of truth. diff --git a/assets/docs/architecture_de.md b/assets/docs/architecture_de.md new file mode 100644 index 0000000..3409d9c --- /dev/null +++ b/assets/docs/architecture_de.md @@ -0,0 +1,55 @@ +# Architektur in einem Absatz + +F∆I hat drei Konzepte, die dir überall begegnen: **Hub**, +**Modul**, **Flow**. + +**Der Hub** ist ein einzelnes Rust-Binary namens `fai`. Er +läuft als gRPC-Server, lädt Module, führt Flows aus und +schreibt Ereignisse in eine SQLite-Datenbank. Ein Binary, kein +Docker, kein Message-Broker, kein Cluster. Du kannst ihn auf +einem Laptop, einem Raspberry Pi oder einem abgeriegelten +KRITIS-Server laufen lassen — immer dasselbe Artefakt. + +**Ein Modul** ist eine kleine WebAssembly-Komponente, die eine +Sache gut macht. Module laufen in einer Sandbox: jedes deklariert +die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen, die es +braucht, und der Hub setzt diese Liste durch. Ein Modul, das +keinen Netzwerkzugriff anfordert, kann das Netzwerk nicht +erreichen. Module kommen als `.fai`-Bundles mit CycloneDX-SBOM +und sigstore-Signatur. + +**Ein Flow** ist eine YAML-Datei mit drei Top-Level-Schlüsseln: +`inputs:`, `steps:`, `outputs:`. Schritte laufen in deklarierter +Reihenfolge, jeder Schritt ruft ein Modul auf, und Ausgaben +eines Schritts können in die Eingaben des nächsten fließen. +Flows sind deterministisch — gleiche Eingaben erzeugen dieselbe +Audit-Spur. + +## Wie die Teile zusammenpassen + +``` +Studio ──gRPC──▶ Hub ──lädt──▶ Modul (WASM) + │ + ├──führt aus──▶ Flow (YAML-Verkettung von Modulen) + │ + └──schreibt────▶ Audit-Log (hash-verkettet, in + SQLite; WORM-1 qua Konstruktion) +``` + +## Was du nicht brauchst + +- Keine Container-Runtime. Der Hub ruft kein Docker oder Podman. + Module sind WASM-Komponenten, in-process geladen. +- Keine externe Datenbank. SQLite kommt mit dem Hub. +- Keine Hintergrunddienste. Ein Prozess; Neustart per + `fai daemon restart` — siehe Diagnose → Daemon-Steuerung. + +## Wo es in dieser App weitergeht + +- **Store** zum Entdecken von Modulen und föderierten MCP/n8n- + Fähigkeiten, Installation in einem Klick. +- **Flows** zum Ausführen der Beispiel-Flows, die mit dem Hub + kommen (z.B. `extract-summarize`). +- **Protokoll** für jedes vom Hub aufgezeichnete Ereignis — das + hash-verkettete Log ist die Single Source of Truth der + Plattform. diff --git a/assets/docs/audit.md b/assets/docs/audit.md new file mode 100644 index 0000000..03822cd --- /dev/null +++ b/assets/docs/audit.md @@ -0,0 +1,69 @@ +# Tamper-evident audit log + +Every action the hub takes goes into one append-only event +log. Module installs, flow runs, approval decisions, channel +switches, daemon restarts — all recorded with the same shape. + +## What an event looks like + +``` +timestamp: 2026-05-08T14:32:11Z +event_id: ev_01H... +event_type: flow.completed +flow_name: extract-summarize +flow_execution: fe_01H... +duration_ms: 1240 +module_name: text.summarize +module_version: 0.1.0 +detail: { engine: "ollama", model_name: "gemma3:4b", + model_digest: "sha256:..." } +prev_event_sha256: 6f4e...8b5f +``` + +The `prev_event_sha256` field is the key. Every new event +hashes its content together with the previous event's hash. To +falsify event N, an attacker would need to rewrite every event +from N+1 onwards — and the chain head would no longer match +what the operator's last verification recorded. + +## What "WORM-1" means + +WORM-1 is our first WORM (write-once-read-many) tier: + +- **Append-only**: the hub never deletes individual events. + The CLI / RPC has a single batch operation, + `clear_event_log`, which wipes the channel and seeds a + `chain.reset` marker carrying reviewer + reason. Refused on + beta / production channels. +- **Tamper-evident**: the chain reveals any retroactive edit. + Verification runs end-to-end on every Doctor-page load and + via `fai admin verify-events`. +- **Self-contained**: SQLite + chain hash is enough; no + external WORM sink needed. + +WORM-2 (Linux append-only file attribute) and WORM-3 (external +WORM sink) are roadmap items, not yet shipped. + +## What you'll see in this app + +- **Doctor → Event log**: live "chain intact" / "tamper + detected" indicator with verified-event count. Click "Verify + now" for an explicit re-check. +- **Audit page**: live stream of recent events with filter + chips (flow / step / module). Click any row to see the full + event JSON. +- **Doctor → Daemon files → Audit-DB**: reveals the SQLite file + in your file manager. + +## Reading the chain by hand + +If you want to verify outside Studio: + +```bash +fai admin verify-events +# Prints "Hash chain intact: N events verified" or names the +# first event whose hash mismatches. +``` + +The chain is the source of truth — every other audit-related +piece of UI in Studio reads from it. diff --git a/assets/docs/audit_de.md b/assets/docs/audit_de.md new file mode 100644 index 0000000..8b0c6a7 --- /dev/null +++ b/assets/docs/audit_de.md @@ -0,0 +1,70 @@ +# Manipulationssicheres Audit-Log + +Jede Aktion des Hubs landet in einem append-only Event-Log. +Modul-Installationen, Flow-Läufe, Freigabe-Entscheidungen, +Kanal-Wechsel, Daemon-Neustarts — alle mit derselben Form +aufgezeichnet. + +## So sieht ein Event aus + +``` +timestamp: 2026-05-08T14:32:11Z +event_id: ev_01H... +event_type: flow.completed +flow_name: extract-summarize +flow_execution: fe_01H... +duration_ms: 1240 +module_name: text.summarize +module_version: 0.1.0 +detail: { engine: "ollama", model_name: "gemma3:4b", + model_digest: "sha256:..." } +prev_event_sha256: 6f4e...8b5f +``` + +Das `prev_event_sha256`-Feld ist der Kern. Jedes neue Event +hasht seinen Inhalt zusammen mit dem Hash des vorherigen. +Wer Event N fälschen will, müsste alle Events ab N+1 neu +schreiben — und der Ketten-Kopf würde nicht mehr zu dem +passen, was der Operator zuletzt verifiziert hat. + +## Was „WORM-1" bedeutet + +WORM-1 ist unsere erste WORM-Stufe (write-once-read-many): + +- **Append-only**: Der Hub löscht keine einzelnen Events. Die + CLI/RPC hat eine einzige Batch-Operation, + `clear_event_log`, die den Kanal komplett leert und einen + `chain.reset`-Marker mit Prüfer + Begründung seedet. Auf + beta-/production-Kanälen verweigert. +- **Manipulationssicher**: Die Kette zeigt jede nachträgliche + Änderung. Verifikation läuft bei jedem Doctor-Page-Load + komplett und über `fai admin verify-events`. +- **Selbst-tragend**: SQLite + Ketten-Hash genügen; keine + externe WORM-Senke nötig. + +WORM-2 (Linux append-only Datei-Attribut) und WORM-3 (externe +WORM-Senke) sind Roadmap-Punkte, noch nicht ausgeliefert. + +## Was du in dieser App siehst + +- **Diagnose → Audit-Log**: Live-Anzeige „Kette intakt" / + „Manipulation erkannt" mit Anzahl verifizierter Events. + „Jetzt prüfen" für explizite Neu-Verifikation. +- **Protokoll-Seite**: Live-Stream der letzten Events mit + Filter-Chips (Flow / Step / Modul). Klick auf eine Zeile + öffnet das vollständige Event-JSON. +- **Diagnose → Daemon-Dateien → Audit-DB**: Zeigt die SQLite- + Datei im Datei-Manager. + +## Die Kette per Hand prüfen + +Wer außerhalb von Studio verifizieren will: + +```bash +fai admin verify-events +# Druckt „Hash-Kette intakt: N Events verifiziert" oder nennt +# das erste Event, dessen Hash nicht passt. +``` + +Die Kette ist die Single Source of Truth — jede andere Audit- +bezogene UI in Studio liest aus ihr. diff --git a/assets/docs/flows.md b/assets/docs/flows.md new file mode 100644 index 0000000..1bbe82e --- /dev/null +++ b/assets/docs/flows.md @@ -0,0 +1,90 @@ +# Flow composition + +A flow is a YAML file with three top-level keys: `inputs:`, +`steps:`, `outputs:`. Read top to bottom, it tells the hub +what kind of inputs to accept, which modules to call in what +order, and what to surface as the final result. + +## Smallest possible flow + +```yaml +name: hello +description: Echo a name back as a greeting. + +inputs: + name: + type: text + +steps: + - id: greet + module: debug.echo + inputs: + payload: "Hello, {{ inputs.name }}" + +outputs: + greeting: "{{ steps.greet.outputs.payload }}" +``` + +`fai run flows/hello.yaml --input name=World` produces: + +```json +{ "greeting": "Hello, World" } +``` + +## Real-world example: extract → summarize + +The bundled `flows/extract-summarize.yaml` chains two modules: + +```yaml +steps: + - id: extract + module: text.extract + inputs: + document: "{{ inputs.file }}" + + - id: summarize + module: text.summarize + inputs: + text: "{{ steps.extract.outputs.text }}" + style: "three bullet points" +``` + +The `{{ steps.extract.outputs.text }}` template plumbs the +output of one step into the next. The flow engine type-coerces +JSON / FileRef / Text per the destination module's manifest, so +you don't need explicit conversion steps. + +## Templating reference (short) + +| Expression | Resolves to | +|------------------------------------|----------------------------| +| `{{ inputs.X }}` | input named X | +| `{{ steps.Y.outputs.Z }}` | output Z of step Y | +| `{{ env.OPENAI_API_KEY }}` | env var (must be declared) | +| `{{ now }}` | ISO-8601 UTC timestamp | +| `{{ flow.execution_id }}` | per-run id (audit-log key) | + +## Audit posture + +Every step emits four events: + +- `step.started` — module name, version, manifest hash +- `step.completed` — duration, output size, well-known + fields auto-elevated (`model_digest`, `engine`, …) +- `step.failed` — error name + message +- `flow.completed` — sum of step durations, total events + +All hash-chained into the same audit log. See the **Audit-log** +explainer for the chain mechanics. + +## What you'll see in this app + +- **Flows page**: list of saved flows under `~/.fai/data/flows/`. + Click `Run`, fill any required inputs, watch the result. +- **Audit page**: per-step events appear within seconds of a + flow finishing. Filter on `flow.` to see only flow-level + rows. +- **Store**: any installed module shows up as available for + `module:` in a flow YAML. Synthetic federated entries + (`mcp..`, `n8n..`) work + the same way. diff --git a/assets/docs/flows_de.md b/assets/docs/flows_de.md new file mode 100644 index 0000000..2725937 --- /dev/null +++ b/assets/docs/flows_de.md @@ -0,0 +1,92 @@ +# Flow-Komposition + +Ein Flow ist eine YAML-Datei mit drei Top-Level-Schlüsseln: +`inputs:`, `steps:`, `outputs:`. Von oben nach unten gelesen, +sagt sie dem Hub, welche Eingaben er akzeptiert, welche Module +in welcher Reihenfolge aufgerufen werden und was am Ende als +Ergebnis nach außen geht. + +## Kleinster möglicher Flow + +```yaml +name: hello +description: Gibt einen Namen als Begrüßung zurück. + +inputs: + name: + type: text + +steps: + - id: greet + module: debug.echo + inputs: + payload: "Hallo, {{ inputs.name }}" + +outputs: + greeting: "{{ steps.greet.outputs.payload }}" +``` + +`fai run flows/hello.yaml --input name=Welt` ergibt: + +```json +{ "greeting": "Hallo, Welt" } +``` + +## Realbeispiel: extract → summarize + +Der mitgelieferte Flow `flows/extract-summarize.yaml` +verkettet zwei Module: + +```yaml +steps: + - id: extract + module: text.extract + inputs: + document: "{{ inputs.file }}" + + - id: summarize + module: text.summarize + inputs: + text: "{{ steps.extract.outputs.text }}" + style: "drei Stichpunkte" +``` + +Das Template `{{ steps.extract.outputs.text }}` reicht die +Ausgabe eines Schritts an den nächsten weiter. Die Flow-Engine +konvertiert JSON / FileRef / Text passend zum Ziel-Modul- +Manifest, ohne explizite Wandlungs-Schritte. + +## Templating-Referenz (kurz) + +| Ausdruck | Löst sich auf zu | +|------------------------------------|------------------------------| +| `{{ inputs.X }}` | Eingabe namens X | +| `{{ steps.Y.outputs.Z }}` | Ausgabe Z von Schritt Y | +| `{{ env.OPENAI_API_KEY }}` | Env-Var (deklariert nötig) | +| `{{ now }}` | ISO-8601-UTC-Zeitstempel | +| `{{ flow.execution_id }}` | Lauf-ID (Audit-Log-Schlüssel)| + +## Audit-Posture + +Jeder Schritt emittiert vier Events: + +- `step.started` — Modul-Name, Version, Manifest-Hash +- `step.completed` — Dauer, Output-Größe, gewhitelistete Felder + automatisch elevated (`model_digest`, `engine`, …) +- `step.failed` — Fehler-Name + Nachricht +- `flow.completed` — Summe der Schritt-Dauern, Events gesamt + +Alle hash-verkettet im gleichen Audit-Log. Siehe Audit-Log- +Erklärung für die Ketten-Mechanik. + +## Was du in dieser App siehst + +- **Flows-Seite**: Liste der gespeicherten Flows unter + `~/.fai/data/flows/`. `Starten` klicken, geforderte Eingaben + ausfüllen, Ergebnis verfolgen. +- **Protokoll-Seite**: Schritt-Events erscheinen Sekunden + nach Flow-Ende. `Flow`-Filter zeigt nur Flow-Level-Zeilen. +- **Store**: Jedes installierte Modul taucht als verfügbares + `module:` in der Flow-YAML auf. Synthetische föderierte + Einträge (`mcp..`, `n8n..`) + funktionieren genauso. diff --git a/assets/docs/security.md b/assets/docs/security.md new file mode 100644 index 0000000..ca6280b --- /dev/null +++ b/assets/docs/security.md @@ -0,0 +1,66 @@ +# Sandbox model + +Every F∆I module runs inside a WebAssembly sandbox with no +ambient access to the host. The module can do exactly what it +declares in its `module.yaml` — and nothing else. + +## What a module declares + +```yaml +permissions: + - net: api.openai.com # outbound HTTPS only + - net: 127.0.0.1:11434 # local Ollama + - fs.read: /var/lib/fai/in # read-only directory + - fs.write: /var/lib/fai/out # writable directory + - env: OPENAI_API_KEY # one specific env var + - hub: invoke # call other modules +``` + +Anything not on this list is impossible at runtime. A module +that forgets to ask for `net:` cannot reach the network at all. +The hub's `WasiCtxBuilder` wires only the declared preopens +and env vars — there is no escape hatch. + +## What the operator controls + +Operators add their own ceiling on top in `~/.fai/config.yaml`: + +```yaml +security: + module_allowlist: # only these modules may load + - "text.*" + module_denylist: # never load these + - "experimental.*" + max_permissions: # cap requests per scope + net: ["api.openai.com"] + fs.read: ["/var/lib/fai"] + require_signatures: true # reject unsigned bundles + require_sbom: true # reject bundles without CycloneDX +``` + +Modules whose declared permissions exceed the operator's +ceiling fail to load with an explicit error. The operator +ceiling is the second gate; the module's own declaration is +the first. + +## What you'll see in this app + +- The **Store** detail sheet for an installed module shows + the declared permissions list with icons (`net:` globe, + `fs.read:` folder, `fs.write:` edit, `env:` terminal). +- The **Doctor** page summary tile shows total module count + and capability count. +- A **flow run** that violates a permission fails with a + named error (`PermissionDenied: net:example.com`) in the + audit log — visible on the Audit page. + +## What we don't have + +- No process-level isolation between modules. They all run + inside one hub process. A WASM trap escalates to a flow-step + failure, not a hub crash, but heap exhaustion in one module + can pressure the others. Pooled instance mode is on the + roadmap. +- No live policy backend (OPA-style runtime decisions). + Permission lists are static at module-load time. Customer + -driven; deferred until a real deployment needs it. diff --git a/assets/docs/security_de.md b/assets/docs/security_de.md new file mode 100644 index 0000000..446313a --- /dev/null +++ b/assets/docs/security_de.md @@ -0,0 +1,68 @@ +# Sandbox-Modell + +Jedes F∆I-Modul läuft in einer WebAssembly-Sandbox ohne +ambienten Hostzugriff. Das Modul kann genau das, was es in +seiner `module.yaml` deklariert — und sonst nichts. + +## Was ein Modul deklariert + +```yaml +permissions: + - net: api.openai.com # nur ausgehendes HTTPS + - net: 127.0.0.1:11434 # lokales Ollama + - fs.read: /var/lib/fai/in # nur-lese-Verzeichnis + - fs.write: /var/lib/fai/out # schreibbares Verzeichnis + - env: OPENAI_API_KEY # eine bestimmte Umgebungsvariable + - hub: invoke # andere Module aufrufen +``` + +Was nicht auf der Liste steht, ist zur Laufzeit unmöglich. Ein +Modul, das `net:` vergisst, kann das Netzwerk gar nicht +erreichen. Der `WasiCtxBuilder` des Hubs verdrahtet nur die +deklarierten Preopens und Env-Vars — keine Hintertür. + +## Was der Operator kontrolliert + +Operatoren setzen ihre eigene Obergrenze in +`~/.fai/config.yaml`: + +```yaml +security: + module_allowlist: # nur diese Module laden + - "text.*" + module_denylist: # diese nie laden + - "experimental.*" + max_permissions: # Cap je Scope + net: ["api.openai.com"] + fs.read: ["/var/lib/fai"] + require_signatures: true # unsignierte Bundles ablehnen + require_sbom: true # Bundles ohne CycloneDX ablehnen +``` + +Module, deren Berechtigungen die Operator-Obergrenze +überschreiten, scheitern beim Laden mit explizitem Fehler. Die +Operator-Obergrenze ist die zweite Schleuse; die Modul-eigene +Deklaration ist die erste. + +## Was du in dieser App siehst + +- Das **Store**-Detail-Sheet eines installierten Moduls zeigt + die Berechtigungsliste mit Icons (`net:` Globus, `fs.read:` + Ordner, `fs.write:` Stift, `env:` Terminal). +- Die **Diagnose**-Seite zeigt Modul-Anzahl und Capability- + Anzahl im Summary. +- Ein **Flow-Lauf**, der eine Berechtigung verletzt, schlägt + mit benanntem Fehler fehl (`PermissionDenied: + net:example.com`) — sichtbar im Protokoll. + +## Was es nicht gibt + +- Keine Prozess-Isolation zwischen Modulen. Alle laufen im + einen Hub-Prozess. Ein WASM-Trap eskaliert zum + Flow-Schritt-Fehler, nicht zum Hub-Crash, aber + Speicher-Überlastung in einem Modul kann die anderen + beeinträchtigen. Pooled-Instance-Modus ist auf der Roadmap. +- Keine Live-Policy-Backend (OPA-artige Laufzeit- + Entscheidungen). Berechtigungslisten sind statisch zur + Modul-Ladezeit. Kunden-getrieben; aufgeschoben bis ein + echter Einsatz es braucht. diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c6a5e94..7278a2b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -20,6 +20,19 @@ "welcomeTrustAuditBody": "Flow-Läufe, Installationen, Deinstallationen, Freigabe-Entscheidungen — alles wird in ein hash-verkettetes Audit-Log geschrieben. Die Diagnose-Seite verifiziert die Kette bei jedem Laden komplett.", "welcomeTrustAirgapTitle": "Air-Gap-tauglich", "welcomeTrustAirgapBody": "Der gesamte Hub steckt in einem einzigen Binary für Linux, macOS und Windows. Sobald ein Modul installiert ist, läuft der Flow, der es nutzt, ohne weiteren Netzwerkzugriff — ideal für regulierte Umgebungen.", + "welcomeDocsHeader": "DOKUMENTATION", + "welcomeDocsBody": "Operator-Erklärungen, inline gerendert. Kein Browser, kein externer Link — air-gap-tauglich.", + "welcomeDocArchitectureTitle": "Architektur in einem Absatz", + "welcomeDocArchitectureBlurb": "Hub, Modul, Flow — und wie die drei zusammenpassen.", + "welcomeDocSecurityTitle": "Sandbox-Modell", + "welcomeDocSecurityBlurb": "Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.", + "welcomeDocAuditTitle": "Manipulationssicheres Audit-Log", + "welcomeDocAuditBlurb": "Wie die Hash-Kette funktioniert und warum WORM-1 für KRITIS reicht.", + "welcomeDocFlowsTitle": "Flow-Komposition", + "welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.", + "welcomeDocClose": "Schließen", + "welcomeDocFailedToLoad": "Doku konnte nicht geladen werden: {error}", + "@welcomeDocFailedToLoad": { "placeholders": { "error": { "type": "String" } } }, "navDoctor": "Diagnose", "navModules": "Module", "navStore": "Store", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5aa0eb0..02bd993 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -21,6 +21,19 @@ "welcomeTrustAuditBody": "Flow runs, install / uninstall actions, approval decisions — all of them write into a hash-chained audit log. The Doctor page verifies the chain end-to-end on every load.", "welcomeTrustAirgapTitle": "Air-gap ready", "welcomeTrustAirgapBody": "The whole hub fits in a single binary that runs on Linux, macOS and Windows. Once a module is installed, the flow it powers runs without further network access — perfect for regulated environments.", + "welcomeDocsHeader": "DOCUMENTATION", + "welcomeDocsBody": "Operator-facing explainers, rendered inline. No browser, no external link — air-gap-friendly.", + "welcomeDocArchitectureTitle": "Architecture in one paragraph", + "welcomeDocArchitectureBlurb": "Hub, Module, Flow — and how the three fit together.", + "welcomeDocSecurityTitle": "Sandbox model", + "welcomeDocSecurityBlurb": "What a module declares, what the operator caps, what the hub enforces.", + "welcomeDocAuditTitle": "Tamper-evident audit log", + "welcomeDocAuditBlurb": "How the hash chain works and why WORM-1 is enough for KRITIS.", + "welcomeDocFlowsTitle": "Flow composition", + "welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.", + "welcomeDocClose": "Close", + "welcomeDocFailedToLoad": "Could not load documentation: {error}", + "@welcomeDocFailedToLoad": { "placeholders": { "error": { "type": "String" } } }, "navDoctor": "Doctor", "navModules": "Modules", "navStore": "Store", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b9df715..9886204 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -206,6 +206,78 @@ abstract class AppLocalizations { /// **'The whole hub fits in a single binary that runs on Linux, macOS and Windows. Once a module is installed, the flow it powers runs without further network access — perfect for regulated environments.'** String get welcomeTrustAirgapBody; + /// No description provided for @welcomeDocsHeader. + /// + /// In en, this message translates to: + /// **'DOCUMENTATION'** + String get welcomeDocsHeader; + + /// No description provided for @welcomeDocsBody. + /// + /// In en, this message translates to: + /// **'Operator-facing explainers, rendered inline. No browser, no external link — air-gap-friendly.'** + String get welcomeDocsBody; + + /// No description provided for @welcomeDocArchitectureTitle. + /// + /// In en, this message translates to: + /// **'Architecture in one paragraph'** + String get welcomeDocArchitectureTitle; + + /// No description provided for @welcomeDocArchitectureBlurb. + /// + /// In en, this message translates to: + /// **'Hub, Module, Flow — and how the three fit together.'** + String get welcomeDocArchitectureBlurb; + + /// No description provided for @welcomeDocSecurityTitle. + /// + /// In en, this message translates to: + /// **'Sandbox model'** + String get welcomeDocSecurityTitle; + + /// No description provided for @welcomeDocSecurityBlurb. + /// + /// In en, this message translates to: + /// **'What a module declares, what the operator caps, what the hub enforces.'** + String get welcomeDocSecurityBlurb; + + /// No description provided for @welcomeDocAuditTitle. + /// + /// In en, this message translates to: + /// **'Tamper-evident audit log'** + String get welcomeDocAuditTitle; + + /// No description provided for @welcomeDocAuditBlurb. + /// + /// In en, this message translates to: + /// **'How the hash chain works and why WORM-1 is enough for KRITIS.'** + String get welcomeDocAuditBlurb; + + /// No description provided for @welcomeDocFlowsTitle. + /// + /// In en, this message translates to: + /// **'Flow composition'** + String get welcomeDocFlowsTitle; + + /// No description provided for @welcomeDocFlowsBlurb. + /// + /// In en, this message translates to: + /// **'YAML basics, templating reference, the extract→summarize example.'** + String get welcomeDocFlowsBlurb; + + /// No description provided for @welcomeDocClose. + /// + /// In en, this message translates to: + /// **'Close'** + String get welcomeDocClose; + + /// No description provided for @welcomeDocFailedToLoad. + /// + /// In en, this message translates to: + /// **'Could not load documentation: {error}'** + String welcomeDocFailedToLoad(String error); + /// No description provided for @navDoctor. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 4c4e0d0..2bab452 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -69,6 +69,49 @@ class AppLocalizationsDe extends AppLocalizations { String get welcomeTrustAirgapBody => 'Der gesamte Hub steckt in einem einzigen Binary für Linux, macOS und Windows. Sobald ein Modul installiert ist, läuft der Flow, der es nutzt, ohne weiteren Netzwerkzugriff — ideal für regulierte Umgebungen.'; + @override + String get welcomeDocsHeader => 'DOKUMENTATION'; + + @override + String get welcomeDocsBody => + 'Operator-Erklärungen, inline gerendert. Kein Browser, kein externer Link — air-gap-tauglich.'; + + @override + String get welcomeDocArchitectureTitle => 'Architektur in einem Absatz'; + + @override + String get welcomeDocArchitectureBlurb => + 'Hub, Modul, Flow — und wie die drei zusammenpassen.'; + + @override + String get welcomeDocSecurityTitle => 'Sandbox-Modell'; + + @override + String get welcomeDocSecurityBlurb => + 'Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.'; + + @override + String get welcomeDocAuditTitle => 'Manipulationssicheres Audit-Log'; + + @override + String get welcomeDocAuditBlurb => + 'Wie die Hash-Kette funktioniert und warum WORM-1 für KRITIS reicht.'; + + @override + String get welcomeDocFlowsTitle => 'Flow-Komposition'; + + @override + String get welcomeDocFlowsBlurb => + 'YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.'; + + @override + String get welcomeDocClose => 'Schließen'; + + @override + String welcomeDocFailedToLoad(String error) { + return 'Doku konnte nicht geladen werden: $error'; + } + @override String get navDoctor => 'Diagnose'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5683ff6..0cb196a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -69,6 +69,49 @@ class AppLocalizationsEn extends AppLocalizations { String get welcomeTrustAirgapBody => 'The whole hub fits in a single binary that runs on Linux, macOS and Windows. Once a module is installed, the flow it powers runs without further network access — perfect for regulated environments.'; + @override + String get welcomeDocsHeader => 'DOCUMENTATION'; + + @override + String get welcomeDocsBody => + 'Operator-facing explainers, rendered inline. No browser, no external link — air-gap-friendly.'; + + @override + String get welcomeDocArchitectureTitle => 'Architecture in one paragraph'; + + @override + String get welcomeDocArchitectureBlurb => + 'Hub, Module, Flow — and how the three fit together.'; + + @override + String get welcomeDocSecurityTitle => 'Sandbox model'; + + @override + String get welcomeDocSecurityBlurb => + 'What a module declares, what the operator caps, what the hub enforces.'; + + @override + String get welcomeDocAuditTitle => 'Tamper-evident audit log'; + + @override + String get welcomeDocAuditBlurb => + 'How the hash chain works and why WORM-1 is enough for KRITIS.'; + + @override + String get welcomeDocFlowsTitle => 'Flow composition'; + + @override + String get welcomeDocFlowsBlurb => + 'YAML basics, templating reference, the extract→summarize example.'; + + @override + String get welcomeDocClose => 'Close'; + + @override + String welcomeDocFailedToLoad(String error) { + return 'Could not load documentation: $error'; + } + @override String get navDoctor => 'Doctor'; diff --git a/lib/main.dart b/lib/main.dart index 07e03d3..bc2c173 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,7 @@ import 'widgets/widgets.dart'; /// Studio's own build version. Bump on every UI commit so the /// running app self-identifies — visible in the sidebar header /// and quick-glance proof that you're seeing the current build. -const String kStudioVersion = '0.37.0'; +const String kStudioVersion = '0.38.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/pages/welcome.dart b/lib/pages/welcome.dart index 8b17895..95a224e 100644 --- a/lib/pages/welcome.dart +++ b/lib/pages/welcome.dart @@ -9,8 +9,12 @@ // getting-started checklist. import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_markdown/flutter_markdown.dart'; +import '../data/system_actions.dart'; import '../l10n/app_localizations.dart'; +import '../theme/theme.dart'; import '../theme/tokens.dart'; class WelcomePage extends StatelessWidget { @@ -48,6 +52,10 @@ class WelcomePage extends StatelessWidget { _SectionLabel(textKey: _SectionLabelKey.trust), SizedBox(height: FaiSpace.md), _TrustPosture(), + SizedBox(height: FaiSpace.xxl), + _SectionLabel(textKey: _SectionLabelKey.docs), + SizedBox(height: FaiSpace.md), + _DocsRow(), ], ), ), @@ -128,7 +136,7 @@ class _Hero extends StatelessWidget { } } -enum _SectionLabelKey { pillars, trust } +enum _SectionLabelKey { pillars, trust, docs } class _SectionLabel extends StatelessWidget { final _SectionLabelKey textKey; @@ -141,6 +149,7 @@ class _SectionLabel extends StatelessWidget { final text = switch (textKey) { _SectionLabelKey.pillars => l.welcomePillarsHeader, _SectionLabelKey.trust => l.welcomeTrustHeader, + _SectionLabelKey.docs => l.welcomeDocsHeader, }; return Text( text, @@ -349,3 +358,291 @@ class _TrustRow extends StatelessWidget { ); } } + +/// Doc-card entries — each links to a bundled markdown file +/// under `assets/docs/`. The slug is the filename stem; the +/// loader picks `.md` or `_de.md` per locale. +class _DocEntry { + final String slug; + final IconData icon; + final String Function(AppLocalizations) title; + final String Function(AppLocalizations) blurb; + const _DocEntry({ + required this.slug, + required this.icon, + required this.title, + required this.blurb, + }); +} + +final List<_DocEntry> _kDocs = <_DocEntry>[ + _DocEntry( + slug: 'architecture', + icon: Icons.account_tree_outlined, + title: (l) => l.welcomeDocArchitectureTitle, + blurb: (l) => l.welcomeDocArchitectureBlurb, + ), + _DocEntry( + slug: 'security', + icon: Icons.shield_outlined, + title: (l) => l.welcomeDocSecurityTitle, + blurb: (l) => l.welcomeDocSecurityBlurb, + ), + _DocEntry( + slug: 'audit', + icon: Icons.verified_outlined, + title: (l) => l.welcomeDocAuditTitle, + blurb: (l) => l.welcomeDocAuditBlurb, + ), + _DocEntry( + slug: 'flows', + icon: Icons.alt_route_outlined, + title: (l) => l.welcomeDocFlowsTitle, + blurb: (l) => l.welcomeDocFlowsBlurb, + ), +]; + +/// Two-by-two doc grid on wide windows, single column on +/// narrow. Each card opens `_DocReaderSheet` for its slug. +class _DocsRow extends StatelessWidget { + const _DocsRow(); + + @override + Widget build(BuildContext context) { + final l = AppLocalizations.of(context)!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l.welcomeDocsBody, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: FaiSpace.md), + LayoutBuilder( + builder: (context, constraints) { + final twoCols = constraints.maxWidth >= 640; + final cardWidth = + twoCols ? (constraints.maxWidth - FaiSpace.md) / 2 : double.infinity; + return Wrap( + spacing: FaiSpace.md, + runSpacing: FaiSpace.md, + children: [ + for (final d in _kDocs) + SizedBox( + width: cardWidth, + child: _DocCard(entry: d), + ), + ], + ); + }, + ), + ], + ); + } +} + +class _DocCard extends StatelessWidget { + final _DocEntry entry; + const _DocCard({required this.entry}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Material( + color: theme.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(FaiRadius.md), + child: InkWell( + onTap: () => _DocReaderSheet.show(context, entry), + borderRadius: BorderRadius.circular(FaiRadius.md), + child: Container( + padding: const EdgeInsets.all(FaiSpace.lg), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(FaiRadius.md), + border: Border.all(color: theme.colorScheme.outlineVariant), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(entry.icon, size: 22, color: theme.colorScheme.primary), + const SizedBox(width: FaiSpace.lg), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entry.title(l), + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + entry.blurb(l), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + height: 1.4, + ), + ), + ], + ), + ), + Icon( + Icons.chevron_right, + size: 18, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ); + } +} + +/// Modal bottom-sheet that loads the markdown for a doc entry +/// from `assets/docs/[_de].md` and renders it inline via +/// flutter_markdown. No browser, no network — KRITIS-friendly. +class _DocReaderSheet extends StatefulWidget { + final _DocEntry entry; + + const _DocReaderSheet({required this.entry}); + + static Future show(BuildContext context, _DocEntry entry) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(FaiRadius.md)), + ), + builder: (_) => _DocReaderSheet(entry: entry), + ); + } + + @override + State<_DocReaderSheet> createState() => _DocReaderSheetState(); +} + +class _DocReaderSheetState extends State<_DocReaderSheet> { + late final Future _content; + + @override + void initState() { + super.initState(); + _content = _load(); + } + + Future _load() async { + final locale = Localizations.localeOf(context).languageCode; + final localised = 'assets/docs/${widget.entry.slug}_$locale.md'; + final fallback = 'assets/docs/${widget.entry.slug}.md'; + try { + return await rootBundle.loadString(localised); + } catch (_) { + return rootBundle.loadString(fallback); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + final maxHeight = MediaQuery.of(context).size.height * 0.85; + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: FaiSpace.sm), + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: theme.colorScheme.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + FaiSpace.xxl, + FaiSpace.sm, + FaiSpace.lg, + FaiSpace.sm, + ), + child: Row( + children: [ + Icon(widget.entry.icon, size: 20, color: theme.colorScheme.primary), + const SizedBox(width: FaiSpace.sm), + Expanded( + child: Text( + widget.entry.title(l), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, size: 18), + visualDensity: VisualDensity.compact, + tooltip: l.welcomeDocClose, + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + const Divider(height: 1), + Flexible( + child: FutureBuilder( + future: _content, + builder: (context, snap) { + if (snap.connectionState == ConnectionState.waiting) { + return const Padding( + padding: EdgeInsets.all(FaiSpace.xxl), + child: Center(child: CircularProgressIndicator()), + ); + } + if (snap.hasError) { + return Padding( + padding: const EdgeInsets.all(FaiSpace.xxl), + child: Text( + l.welcomeDocFailedToLoad(snap.error.toString()), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.error, + ), + ), + ); + } + return Markdown( + data: snap.data ?? '', + padding: const EdgeInsets.all(FaiSpace.xxl), + selectable: true, + onTapLink: (text, href, title) async { + if (href != null && href.isNotEmpty) { + await SystemActions.openInOs(href); + } + }, + styleSheet: MarkdownStyleSheet.fromTheme(theme).copyWith( + p: theme.textTheme.bodyMedium?.copyWith(height: 1.5), + code: FaiTheme.mono( + size: 12, + color: theme.colorScheme.onSurface, + ), + codeblockDecoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(FaiRadius.sm), + ), + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 829e3d8..140dd44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fai_studio description: "F∆I Studio — desktop GUI for the F∆I hub" publish_to: 'none' -version: 0.37.0 +version: 0.38.0 environment: sdk: ^3.11.0-200.1.beta @@ -34,3 +34,9 @@ dev_dependencies: flutter: uses-material-design: true generate: true + # Bundled operator-facing documentation rendered inline by + # the Welcome page. Each doc has a `_de.md` sibling for the + # German locale; the loader picks the right pair at runtime. + # Air-gap-friendly — no network calls to read these. + assets: + - assets/docs/