From 28fafce7dc169ea902e4dd06b64dfe45f8fdebc2 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Tue, 9 Jun 2026 01:57:46 +0200 Subject: [PATCH] feat(studio): localise Settings sidebar + add inline help docs + Approvals doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three operator-UX gaps closed: - Settings dialog's six-category sidebar (General / Appearance / System AI / Integrations / Security / Maintenance) was hardcoded English; now flows through AppLocalizations ("Allgemein" / "Darstellung" / "System-KI" / "Integrationen" / "Sicherheit" / "Wartung"). Same for the per-panel title + description. - Per-channel daemon-action labels ('enable autostart' / 'disable autostart' / 'daemon restart' etc.) and the OK / Failed result line in the toast also moved to l10n, so the system-action feedback reads as one language. - New 'Approvals' doc bundle (en + de) under assets/docs/, registered as a fifth doc card on Welcome plus exposed via the new public helper. The Approvals, Audit and Doctor app-bars grow a Help icon button next to Refresh that opens the matching doc in the existing bottom-sheet reader — no extra screen, no learning curve. Studio bumped to 0.67.0; editor path-override pulls in 0.20.1 (flow-list row polish). Signed-off-by: flemming-it --- assets/docs/approvals.md | 81 +++++++++++++ assets/docs/approvals_de.md | 88 ++++++++++++++ lib/l10n/app_de.arb | 38 ++++++ lib/l10n/app_en.arb | 38 ++++++ lib/l10n/app_localizations.dart | 168 +++++++++++++++++++++++++++ lib/l10n/app_localizations_de.dart | 93 +++++++++++++++ lib/l10n/app_localizations_en.dart | 93 +++++++++++++++ lib/main.dart | 2 +- lib/pages/approvals.dart | 6 + lib/pages/audit.dart | 6 + lib/pages/doctor.dart | 19 ++- lib/pages/welcome.dart | 19 +++ lib/widgets/fai_settings_dialog.dart | 62 +++++----- pubspec.lock | 2 +- pubspec.yaml | 2 +- 15 files changed, 681 insertions(+), 36 deletions(-) create mode 100644 assets/docs/approvals.md create mode 100644 assets/docs/approvals_de.md diff --git a/assets/docs/approvals.md b/assets/docs/approvals.md new file mode 100644 index 0000000..8a96cf6 --- /dev/null +++ b/assets/docs/approvals.md @@ -0,0 +1,81 @@ +# Approvals + +An approval is a deliberate human-in-the-loop checkpoint inside +a flow. When a step needs a human decision before it proceeds, +the flow pauses at that step until a reviewer approves or +rejects it. + +## When to use one + +Add an approval step before: + +- writing to a system of record (cases, contracts, mailings) +- spending money (LLM tokens above a threshold, API quotas) +- destructive actions (delete, archive, send) +- anything the operator's compliance owner asked you to gate + +The approval is a recorded event — every approval shows up in +the audit log with the reviewer, decision, and reason. + +## How a flow declares one + +```yaml +steps: + - id: classify + use: text.classify@^1 + + - id: review + use: system.approval@^0 + with: + title: "Review the classifier output" + details: "$classify.response" + reviewer: "$inputs.reviewer" + timeout_seconds: 600 + + - id: write + use: erp.write@^1 + with: + record: "$review.payload" +``` + +`system.approval` is a built-in capability — no module install +needed. It blocks the flow's run until the matching approval +record reaches a `decision` (approved / rejected) or the +configured `timeout_seconds` elapses. + +## How an operator answers one + +The Approvals page lists every pending approval. Click an +entry, read the title + details, decide: + +- **Approve** — the flow resumes from this step with the + approval's `payload` as the step output. +- **Reject** — the flow fails at this step with the reviewer's + reason; the audit log records the rejection. + +## Timeouts + +If no decision arrives within `timeout_seconds`, the engine +returns `ApprovalTimedOut` and the flow fails. Set a timeout +that reflects how long a reviewer can realistically take. + +## What gets logged + +Every approval write hits the audit log: + +- approval requested (with the title + details snapshot) +- reviewer-decision recorded (with reason on reject) +- payload + signature on accept + +The hash chain catches tampering — an entry that was approved +yesterday can't be quietly flipped to rejected today without +breaking the chain. + +## Architecture trade-off + +Approvals are stored alongside events in the SQLite audit DB. +For higher-assurance deployments, mirror the audit DB to a +WORM sink (Compliance Gap 3, option 2). The approval contract +is the same either way — the WORM sink just makes the audit +trail tamper-evident to outside reviewers, not just to the +hub itself. diff --git a/assets/docs/approvals_de.md b/assets/docs/approvals_de.md new file mode 100644 index 0000000..af97496 --- /dev/null +++ b/assets/docs/approvals_de.md @@ -0,0 +1,88 @@ +# Freigaben + +Eine Freigabe ist ein bewusster Human-in-the-Loop-Checkpoint +innerhalb eines Flows. Wenn ein Step eine menschliche +Entscheidung benötigt, pausiert der Flow an dieser Stelle, bis +ein Reviewer freigibt oder ablehnt. + +## Wann nutzen? + +Setze eine Freigabe vor: + +- Schreibzugriffe auf Bestandssysteme (Akten, Verträge, + Versand) +- Geldausgaben (LLM-Tokens jenseits einer Schwelle, API-Quotas) +- destruktiven Aktionen (löschen, archivieren, senden) +- alles, was dein Compliance-Verantwortlicher abgesichert sehen + möchte + +Die Freigabe ist ein protokolliertes Ereignis — jeder +Freigabe-Vorgang landet im Audit-Log mit Reviewer, +Entscheidung und Begründung. + +## So deklariert ein Flow eine Freigabe + +```yaml +steps: + - id: classify + use: text.classify@^1 + + - id: review + use: system.approval@^0 + with: + title: "Klassifikations-Output prüfen" + details: "$classify.response" + reviewer: "$inputs.reviewer" + timeout_seconds: 600 + + - id: write + use: erp.write@^1 + with: + record: "$review.payload" +``` + +`system.approval` ist eine eingebaute Capability — keine +Modul-Installation nötig. Sie blockiert den Flow-Lauf, bis der +zugehörige Freigabe-Eintrag eine `decision` erhält +(freigegeben / abgelehnt) oder das konfigurierte +`timeout_seconds` ausläuft. + +## So beantwortet der Operator eine Freigabe + +Die Freigaben-Seite listet jede ausstehende Freigabe. Klick +auf einen Eintrag, Titel + Details lesen, entscheiden: + +- **Freigeben** — der Flow läuft ab diesem Step weiter, das + `payload` der Freigabe wird zum Step-Output. +- **Ablehnen** — der Flow scheitert an diesem Step mit der + Begründung des Reviewers; die Ablehnung wird im Audit-Log + protokolliert. + +## Timeouts + +Wenn keine Entscheidung innerhalb `timeout_seconds` eintrifft, +liefert die Engine `ApprovalTimedOut` und der Flow scheitert. +Setze einen Timeout, der zeigt wie lange ein Reviewer +realistisch brauchen darf. + +## Was protokolliert wird + +Jeder Schreibvorgang einer Freigabe landet im Audit-Log: + +- Freigabe angefordert (mit Titel + Details-Snapshot) +- Reviewer-Entscheidung aufgezeichnet (mit Begründung bei + Ablehnung) +- Payload + Signatur bei Freigabe + +Die Hash-Chain fängt Manipulationen ab — ein gestern +freigegebener Eintrag kann nicht heimlich heute auf +"abgelehnt" gedreht werden, ohne die Kette zu brechen. + +## Architektur-Abwägung + +Freigaben liegen in derselben SQLite-Audit-DB wie die +Ereignisse. Für Deployments mit höherem Sicherheitsbedarf +spiegelt man die Audit-DB auf eine WORM-Senke (Compliance-Gap +3, Option 2). Der Freigabe-Kontrakt bleibt gleich — die +WORM-Senke macht den Audit-Trail nur tamper-evident gegenüber +externen Prüfern, nicht nur für den Hub selbst. diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 6acd43f..4b8d4a2 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -27,6 +27,9 @@ "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.", + "welcomeDocApprovalsTitle": "Freigaben", + "welcomeDocApprovalsBlurb": "Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.", + "helpTooltip": "Hilfe", "welcomeDocClose": "Schließen", "welcomeDocFailedToLoad": "Doku konnte nicht geladen werden: {error}", "@welcomeDocFailedToLoad": { @@ -1449,6 +1452,41 @@ } }, "doctorPathStudioErrors": "Studio-Fehler", + "settingsCategoryGeneral": "Allgemein", + "settingsCategoryAppearance": "Darstellung", + "settingsCategoryAi": "System-KI", + "settingsCategoryIntegrations": "Integrationen", + "settingsCategorySecurity": "Sicherheit", + "settingsCategoryMaintenance": "Wartung", + "settingsAiPanelTitle": "System-KI", + "settingsAiPanelBody": "Operator-verwalteter LLM-Endpunkt, den Studio für die KI-Helfer nutzt (System-KI-Edit, Install-Vorschläge, Doctor-Zusammenfassung). Kein Flow ruft das jemals auf; Module sprechen mit ihrem eigenen LLM.", + "settingsIntegrationsPanelTitle": "Integrationen", + "settingsIntegrationsPanelBody": "Externe Tool-Server, deren Capabilities der Hub föderiert. MCP-Server bieten Tool-Endpunkte; n8n-Endpunkte stellen gehostete Workflows bereit.", + "settingsSecurityPanelTitle": "Sicherheit & Zugangsdaten", + "settingsSecurityPanelBody": "Tokens, die der Hub für dich verwahrt. Liegen in ~/.fai/ mit Modus 0600; werden nie als Telemetrie versendet.", + "settingsMaintenancePanelTitle": "Wartung", + "settingsMaintenancePanelBody": "Destruktive Aktionen, abgesichert durch explizite Bestätigungs-Dialoge. Nutzen z.B. beim Re-Pilot-Testen oder Debuggen.", + "tooltipMoveUp": "Nach oben", + "tooltipMoveDown": "Nach unten", + "tooltipRemove": "Entfernen", + "daemonActionEnableAutostart": "Autostart aktivieren", + "daemonActionDisableAutostart": "Autostart deaktivieren", + "daemonActionRestart": "Daemon neu starten", + "daemonActionStart": "Daemon starten", + "daemonActionStop": "Daemon stoppen", + "daemonActionStatus": "Daemon-Status", + "daemonActionResultOk": "OK · {label}", + "@daemonActionResultOk": { + "placeholders": { + "label": {"type": "String"} + } + }, + "daemonActionResultFailed": "Fehlgeschlagen · {label}", + "@daemonActionResultFailed": { + "placeholders": { + "label": {"type": "String"} + } + }, "addSourceTitle": "Modul-Quelle hinzufügen", "addSourceIntro": "`{capability}` ist nicht im öffentlichen Store. Zeig dem Hub eine `.fai`-Bundle-URL oder einen lokalen Bundle-Pfad — der Hub lädt es herunter, prüft (sha256 + Signatur) und installiert.", "@addSourceIntro": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2226206..92f47fb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -30,6 +30,9 @@ "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.", + "welcomeDocApprovalsTitle": "Approvals", + "welcomeDocApprovalsBlurb": "Human-in-the-loop checkpoints — when to use them, how the audit log records them.", + "helpTooltip": "Help", "welcomeDocClose": "Close", "welcomeDocFailedToLoad": "Could not load documentation: {error}", "@welcomeDocFailedToLoad": { @@ -1452,6 +1455,41 @@ } }, "doctorPathStudioErrors": "Studio errors", + "settingsCategoryGeneral": "General", + "settingsCategoryAppearance": "Appearance", + "settingsCategoryAi": "System AI", + "settingsCategoryIntegrations": "Integrations", + "settingsCategorySecurity": "Security", + "settingsCategoryMaintenance": "Maintenance", + "settingsAiPanelTitle": "System AI", + "settingsAiPanelBody": "Operator-managed LLM endpoint used by Studio for the AI helpers (system-ai-edit, install-suggest, doctor-summary). No flow ever calls this; modules speak to their own LLM.", + "settingsIntegrationsPanelTitle": "Integrations", + "settingsIntegrationsPanelBody": "External tool servers the hub federates capabilities from. MCP servers expose tool-shaped endpoints; n8n endpoints expose hosted workflows.", + "settingsSecurityPanelTitle": "Security & credentials", + "settingsSecurityPanelBody": "Tokens the hub holds on the operator's behalf. Stored in ~/.fai/ with mode 0600; never sent in telemetry.", + "settingsMaintenancePanelTitle": "Maintenance", + "settingsMaintenancePanelBody": "Destructive operations gated behind explicit confirm dialogs. Use when re-pilot-testing or debugging.", + "tooltipMoveUp": "Move up", + "tooltipMoveDown": "Move down", + "tooltipRemove": "Remove", + "daemonActionEnableAutostart": "enable autostart", + "daemonActionDisableAutostart": "disable autostart", + "daemonActionRestart": "daemon restart", + "daemonActionStart": "daemon start", + "daemonActionStop": "daemon stop", + "daemonActionStatus": "daemon status", + "daemonActionResultOk": "OK · {label}", + "@daemonActionResultOk": { + "placeholders": { + "label": {"type": "String"} + } + }, + "daemonActionResultFailed": "Failed · {label}", + "@daemonActionResultFailed": { + "placeholders": { + "label": {"type": "String"} + } + }, "addSourceTitle": "Add module source", "addSourceIntro": "`{capability}` is not in the public store. Point the hub at a `.fai` bundle URL or a local bundle path; the hub downloads, verifies (sha256 + signature) and installs it.", "@addSourceIntro": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d70ac45..6891686 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -260,6 +260,24 @@ abstract class AppLocalizations { /// **'YAML basics, templating reference, the extract→summarize example.'** String get welcomeDocFlowsBlurb; + /// No description provided for @welcomeDocApprovalsTitle. + /// + /// In en, this message translates to: + /// **'Approvals'** + String get welcomeDocApprovalsTitle; + + /// No description provided for @welcomeDocApprovalsBlurb. + /// + /// In en, this message translates to: + /// **'Human-in-the-loop checkpoints — when to use them, how the audit log records them.'** + String get welcomeDocApprovalsBlurb; + + /// No description provided for @helpTooltip. + /// + /// In en, this message translates to: + /// **'Help'** + String get helpTooltip; + /// No description provided for @welcomeDocClose. /// /// In en, this message translates to: @@ -3968,6 +3986,156 @@ abstract class AppLocalizations { /// **'Studio errors'** String get doctorPathStudioErrors; + /// No description provided for @settingsCategoryGeneral. + /// + /// In en, this message translates to: + /// **'General'** + String get settingsCategoryGeneral; + + /// No description provided for @settingsCategoryAppearance. + /// + /// In en, this message translates to: + /// **'Appearance'** + String get settingsCategoryAppearance; + + /// No description provided for @settingsCategoryAi. + /// + /// In en, this message translates to: + /// **'System AI'** + String get settingsCategoryAi; + + /// No description provided for @settingsCategoryIntegrations. + /// + /// In en, this message translates to: + /// **'Integrations'** + String get settingsCategoryIntegrations; + + /// No description provided for @settingsCategorySecurity. + /// + /// In en, this message translates to: + /// **'Security'** + String get settingsCategorySecurity; + + /// No description provided for @settingsCategoryMaintenance. + /// + /// In en, this message translates to: + /// **'Maintenance'** + String get settingsCategoryMaintenance; + + /// No description provided for @settingsAiPanelTitle. + /// + /// In en, this message translates to: + /// **'System AI'** + String get settingsAiPanelTitle; + + /// No description provided for @settingsAiPanelBody. + /// + /// In en, this message translates to: + /// **'Operator-managed LLM endpoint used by Studio for the AI helpers (system-ai-edit, install-suggest, doctor-summary). No flow ever calls this; modules speak to their own LLM.'** + String get settingsAiPanelBody; + + /// No description provided for @settingsIntegrationsPanelTitle. + /// + /// In en, this message translates to: + /// **'Integrations'** + String get settingsIntegrationsPanelTitle; + + /// No description provided for @settingsIntegrationsPanelBody. + /// + /// In en, this message translates to: + /// **'External tool servers the hub federates capabilities from. MCP servers expose tool-shaped endpoints; n8n endpoints expose hosted workflows.'** + String get settingsIntegrationsPanelBody; + + /// No description provided for @settingsSecurityPanelTitle. + /// + /// In en, this message translates to: + /// **'Security & credentials'** + String get settingsSecurityPanelTitle; + + /// No description provided for @settingsSecurityPanelBody. + /// + /// In en, this message translates to: + /// **'Tokens the hub holds on the operator\'s behalf. Stored in ~/.fai/ with mode 0600; never sent in telemetry.'** + String get settingsSecurityPanelBody; + + /// No description provided for @settingsMaintenancePanelTitle. + /// + /// In en, this message translates to: + /// **'Maintenance'** + String get settingsMaintenancePanelTitle; + + /// No description provided for @settingsMaintenancePanelBody. + /// + /// In en, this message translates to: + /// **'Destructive operations gated behind explicit confirm dialogs. Use when re-pilot-testing or debugging.'** + String get settingsMaintenancePanelBody; + + /// No description provided for @tooltipMoveUp. + /// + /// In en, this message translates to: + /// **'Move up'** + String get tooltipMoveUp; + + /// No description provided for @tooltipMoveDown. + /// + /// In en, this message translates to: + /// **'Move down'** + String get tooltipMoveDown; + + /// No description provided for @tooltipRemove. + /// + /// In en, this message translates to: + /// **'Remove'** + String get tooltipRemove; + + /// No description provided for @daemonActionEnableAutostart. + /// + /// In en, this message translates to: + /// **'enable autostart'** + String get daemonActionEnableAutostart; + + /// No description provided for @daemonActionDisableAutostart. + /// + /// In en, this message translates to: + /// **'disable autostart'** + String get daemonActionDisableAutostart; + + /// No description provided for @daemonActionRestart. + /// + /// In en, this message translates to: + /// **'daemon restart'** + String get daemonActionRestart; + + /// No description provided for @daemonActionStart. + /// + /// In en, this message translates to: + /// **'daemon start'** + String get daemonActionStart; + + /// No description provided for @daemonActionStop. + /// + /// In en, this message translates to: + /// **'daemon stop'** + String get daemonActionStop; + + /// No description provided for @daemonActionStatus. + /// + /// In en, this message translates to: + /// **'daemon status'** + String get daemonActionStatus; + + /// No description provided for @daemonActionResultOk. + /// + /// In en, this message translates to: + /// **'OK · {label}'** + String daemonActionResultOk(String label); + + /// No description provided for @daemonActionResultFailed. + /// + /// In en, this message translates to: + /// **'Failed · {label}'** + String daemonActionResultFailed(String label); + /// No description provided for @addSourceTitle. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 87806fe..ba94f3c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -101,6 +101,16 @@ class AppLocalizationsDe extends AppLocalizations { String get welcomeDocFlowsBlurb => 'YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.'; + @override + String get welcomeDocApprovalsTitle => 'Freigaben'; + + @override + String get welcomeDocApprovalsBlurb => + 'Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.'; + + @override + String get helpTooltip => 'Hilfe'; + @override String get welcomeDocClose => 'Schließen'; @@ -2323,6 +2333,89 @@ class AppLocalizationsDe extends AppLocalizations { @override String get doctorPathStudioErrors => 'Studio-Fehler'; + @override + String get settingsCategoryGeneral => 'Allgemein'; + + @override + String get settingsCategoryAppearance => 'Darstellung'; + + @override + String get settingsCategoryAi => 'System-KI'; + + @override + String get settingsCategoryIntegrations => 'Integrationen'; + + @override + String get settingsCategorySecurity => 'Sicherheit'; + + @override + String get settingsCategoryMaintenance => 'Wartung'; + + @override + String get settingsAiPanelTitle => 'System-KI'; + + @override + String get settingsAiPanelBody => + 'Operator-verwalteter LLM-Endpunkt, den Studio für die KI-Helfer nutzt (System-KI-Edit, Install-Vorschläge, Doctor-Zusammenfassung). Kein Flow ruft das jemals auf; Module sprechen mit ihrem eigenen LLM.'; + + @override + String get settingsIntegrationsPanelTitle => 'Integrationen'; + + @override + String get settingsIntegrationsPanelBody => + 'Externe Tool-Server, deren Capabilities der Hub föderiert. MCP-Server bieten Tool-Endpunkte; n8n-Endpunkte stellen gehostete Workflows bereit.'; + + @override + String get settingsSecurityPanelTitle => 'Sicherheit & Zugangsdaten'; + + @override + String get settingsSecurityPanelBody => + 'Tokens, die der Hub für dich verwahrt. Liegen in ~/.fai/ mit Modus 0600; werden nie als Telemetrie versendet.'; + + @override + String get settingsMaintenancePanelTitle => 'Wartung'; + + @override + String get settingsMaintenancePanelBody => + 'Destruktive Aktionen, abgesichert durch explizite Bestätigungs-Dialoge. Nutzen z.B. beim Re-Pilot-Testen oder Debuggen.'; + + @override + String get tooltipMoveUp => 'Nach oben'; + + @override + String get tooltipMoveDown => 'Nach unten'; + + @override + String get tooltipRemove => 'Entfernen'; + + @override + String get daemonActionEnableAutostart => 'Autostart aktivieren'; + + @override + String get daemonActionDisableAutostart => 'Autostart deaktivieren'; + + @override + String get daemonActionRestart => 'Daemon neu starten'; + + @override + String get daemonActionStart => 'Daemon starten'; + + @override + String get daemonActionStop => 'Daemon stoppen'; + + @override + String get daemonActionStatus => 'Daemon-Status'; + + @override + String daemonActionResultOk(String label) { + return 'OK · $label'; + } + + @override + String daemonActionResultFailed(String label) { + return 'Fehlgeschlagen · $label'; + } + @override String get addSourceTitle => 'Modul-Quelle hinzufügen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a8c4c9e..6d8d401 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -101,6 +101,16 @@ class AppLocalizationsEn extends AppLocalizations { String get welcomeDocFlowsBlurb => 'YAML basics, templating reference, the extract→summarize example.'; + @override + String get welcomeDocApprovalsTitle => 'Approvals'; + + @override + String get welcomeDocApprovalsBlurb => + 'Human-in-the-loop checkpoints — when to use them, how the audit log records them.'; + + @override + String get helpTooltip => 'Help'; + @override String get welcomeDocClose => 'Close'; @@ -2328,6 +2338,89 @@ class AppLocalizationsEn extends AppLocalizations { @override String get doctorPathStudioErrors => 'Studio errors'; + @override + String get settingsCategoryGeneral => 'General'; + + @override + String get settingsCategoryAppearance => 'Appearance'; + + @override + String get settingsCategoryAi => 'System AI'; + + @override + String get settingsCategoryIntegrations => 'Integrations'; + + @override + String get settingsCategorySecurity => 'Security'; + + @override + String get settingsCategoryMaintenance => 'Maintenance'; + + @override + String get settingsAiPanelTitle => 'System AI'; + + @override + String get settingsAiPanelBody => + 'Operator-managed LLM endpoint used by Studio for the AI helpers (system-ai-edit, install-suggest, doctor-summary). No flow ever calls this; modules speak to their own LLM.'; + + @override + String get settingsIntegrationsPanelTitle => 'Integrations'; + + @override + String get settingsIntegrationsPanelBody => + 'External tool servers the hub federates capabilities from. MCP servers expose tool-shaped endpoints; n8n endpoints expose hosted workflows.'; + + @override + String get settingsSecurityPanelTitle => 'Security & credentials'; + + @override + String get settingsSecurityPanelBody => + 'Tokens the hub holds on the operator\'s behalf. Stored in ~/.fai/ with mode 0600; never sent in telemetry.'; + + @override + String get settingsMaintenancePanelTitle => 'Maintenance'; + + @override + String get settingsMaintenancePanelBody => + 'Destructive operations gated behind explicit confirm dialogs. Use when re-pilot-testing or debugging.'; + + @override + String get tooltipMoveUp => 'Move up'; + + @override + String get tooltipMoveDown => 'Move down'; + + @override + String get tooltipRemove => 'Remove'; + + @override + String get daemonActionEnableAutostart => 'enable autostart'; + + @override + String get daemonActionDisableAutostart => 'disable autostart'; + + @override + String get daemonActionRestart => 'daemon restart'; + + @override + String get daemonActionStart => 'daemon start'; + + @override + String get daemonActionStop => 'daemon stop'; + + @override + String get daemonActionStatus => 'daemon status'; + + @override + String daemonActionResultOk(String label) { + return 'OK · $label'; + } + + @override + String daemonActionResultFailed(String label) { + return 'Failed · $label'; + } + @override String get addSourceTitle => 'Add module source'; diff --git a/lib/main.dart b/lib/main.dart index d5f8869..0973317 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,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.66.0'; +const String kStudioVersion = '0.67.0'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/pages/approvals.dart b/lib/pages/approvals.dart index 3efddf0..f622480 100644 --- a/lib/pages/approvals.dart +++ b/lib/pages/approvals.dart @@ -8,6 +8,7 @@ import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; import '../widgets/widgets.dart'; +import 'welcome.dart' show showFaiDoc; class ApprovalsPage extends StatefulWidget { const ApprovalsPage({super.key}); @@ -213,6 +214,11 @@ class _ApprovalsPageState extends State ], ), actions: [ + IconButton( + icon: const Icon(Icons.help_outline, size: 18), + tooltip: AppLocalizations.of(context)!.helpTooltip, + onPressed: () => showFaiDoc(context, 'approvals'), + ), IconButton( icon: const Icon(Icons.refresh, size: 18), tooltip: AppLocalizations.of(context)!.approvalsReloadTooltip, diff --git a/lib/pages/audit.dart b/lib/pages/audit.dart index fdedb88..ea6a0c5 100644 --- a/lib/pages/audit.dart +++ b/lib/pages/audit.dart @@ -10,6 +10,7 @@ import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; import '../widgets/widgets.dart'; +import 'welcome.dart' show showFaiDoc; class AuditPage extends StatefulWidget { const AuditPage({super.key}); @@ -102,6 +103,11 @@ class _AuditPageState extends State { onChanged: (v) => setState(() => _typeFilter = v), ), ), + IconButton( + icon: const Icon(Icons.help_outline, size: 18), + tooltip: AppLocalizations.of(context)!.helpTooltip, + onPressed: () => showFaiDoc(context, 'audit'), + ), IconButton( icon: const Icon(Icons.delete_sweep_outlined, size: 18), tooltip: AppLocalizations.of(context)!.auditClearLogTooltip, diff --git a/lib/pages/doctor.dart b/lib/pages/doctor.dart index 737fb9a..247934b 100644 --- a/lib/pages/doctor.dart +++ b/lib/pages/doctor.dart @@ -8,6 +8,7 @@ import '../l10n/app_localizations.dart'; import '../theme/theme.dart'; import '../theme/tokens.dart'; import '../widgets/widgets.dart'; +import 'welcome.dart' show showFaiDoc; class DoctorPage extends StatefulWidget { const DoctorPage({super.key}); @@ -36,6 +37,11 @@ class _DoctorPageState extends State { appBar: AppBar( title: Text(AppLocalizations.of(context)!.doctorTitle), actions: [ + IconButton( + icon: const Icon(Icons.help_outline, size: 18), + tooltip: AppLocalizations.of(context)!.helpTooltip, + onPressed: () => showFaiDoc(context, 'architecture'), + ), IconButton( icon: const Icon(Icons.refresh, size: 18), tooltip: AppLocalizations.of(context)!.doctorRecheckTooltip, @@ -504,12 +510,13 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { }); final r = await action(); if (!mounted) return; + final l = AppLocalizations.of(context)!; setState(() { _busy = false; _output = (r.ok - ? 'OK · $label\n${r.stdout}' - : 'Failed · $label\n${r.stderr.isEmpty ? r.stdout : r.stderr}') + ? '${l.daemonActionResultOk(label)}\n${r.stdout}' + : '${l.daemonActionResultFailed(label)}\n${r.stderr.isEmpty ? r.stdout : r.stderr}') .trim(); }); // Refresh the status pill — restart / stop / start all @@ -594,7 +601,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { onPressed: _busy ? null : () => _run( - 'daemon restart', + l.daemonActionRestart, () => SystemActions.faiDaemon(['restart']), ), icon: const Icon(Icons.restart_alt, size: 16), @@ -605,7 +612,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { onPressed: _busy ? null : () => _run( - 'daemon start', + l.daemonActionStart, () => SystemActions.faiDaemon(['start']), ), icon: const Icon(Icons.play_arrow, size: 16), @@ -615,7 +622,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { onPressed: _busy ? null : () => _run( - 'daemon stop', + l.daemonActionStop, () => SystemActions.faiDaemon(['stop']), ), icon: const Icon(Icons.stop_circle_outlined, size: 16), @@ -625,7 +632,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> { onPressed: _busy ? null : () => _run( - 'daemon status', + l.daemonActionStatus, () => SystemActions.faiDaemon(['status']), ), icon: const Icon(Icons.health_and_safety_outlined, size: 16), diff --git a/lib/pages/welcome.dart b/lib/pages/welcome.dart index 9767ed1..d5c6c29 100644 --- a/lib/pages/welcome.dart +++ b/lib/pages/welcome.dart @@ -829,8 +829,27 @@ final List<_DocEntry> _kDocs = <_DocEntry>[ title: (l) => l.welcomeDocFlowsTitle, blurb: (l) => l.welcomeDocFlowsBlurb, ), + _DocEntry( + slug: 'approvals', + icon: Icons.gavel_outlined, + title: (l) => l.welcomeDocApprovalsTitle, + blurb: (l) => l.welcomeDocApprovalsBlurb, + ), ]; +/// Public entry-point for the doc-reader sheet. Pass a slug +/// ("approvals", "audit", "security", "architecture", "flows") +/// and the bottom sheet opens with the localized markdown. +/// Returns null when the slug isn't registered — caller can +/// log + show a SnackBar. +Future? showFaiDoc(BuildContext context, String slug) { + final entry = _kDocs.firstWhere( + (d) => d.slug == slug, + orElse: () => _kDocs.first, + ); + return _DocReaderSheet.show(context, entry); +} + /// Two-by-two doc grid on wide windows, single column on /// narrow. Each card opens `_DocReaderSheet` for its slug. class _DocsRow extends StatelessWidget { diff --git a/lib/widgets/fai_settings_dialog.dart b/lib/widgets/fai_settings_dialog.dart index 685f842..f7d70b1 100644 --- a/lib/widgets/fai_settings_dialog.dart +++ b/lib/widgets/fai_settings_dialog.dart @@ -311,11 +311,12 @@ class _FaiSettingsDialogState extends State { }); final r = await action(); if (!mounted) return; + final l = AppLocalizations.of(context)!; setState(() { _saving = false; _channelToast = r.ok - ? 'OK · $label\n${r.stdout.trim()}' - : 'Failed · $label\n${(r.stderr.isEmpty ? r.stdout : r.stderr).trim()}'; + ? '${l.daemonActionResultOk(label)}\n${r.stdout.trim()}' + : '${l.daemonActionResultFailed(label)}\n${(r.stderr.isEmpty ? r.stdout : r.stderr).trim()}'; }); } @@ -528,13 +529,13 @@ class _FaiSettingsDialogState extends State { onEnableAutostart: _saving ? null : () => _runDaemon( - 'enable autostart', + l.daemonActionEnableAutostart, () => SystemActions.faiDaemonEnable(ch.name), ), onDisableAutostart: _saving ? null : () => _runDaemon( - 'disable autostart', + l.daemonActionDisableAutostart, () => SystemActions.faiDaemonDisable(ch.name), ), ), @@ -562,10 +563,8 @@ class _FaiSettingsDialogState extends State { List _aiPanel(AppLocalizations l, ThemeData theme) { return [ _panelTitle( - 'System AI', - 'Operator-managed LLM endpoint used by Studio for the AI ' - 'helpers (system-ai-edit, install-suggest, doctor-summary). ' - 'No flow ever calls this; modules speak to their own LLM.', + l.settingsAiPanelTitle, + l.settingsAiPanelBody, theme, ), if (_aiStatus == null) @@ -586,10 +585,8 @@ class _FaiSettingsDialogState extends State { List _integrationsPanel(AppLocalizations l, ThemeData theme) { return [ _panelTitle( - 'Integrations', - 'External tool servers the hub federates capabilities from. ' - 'MCP servers expose tool-shaped endpoints; n8n endpoints ' - 'expose hosted workflows.', + l.settingsIntegrationsPanelTitle, + l.settingsIntegrationsPanelBody, theme, ), if (_mcpClients == null) @@ -618,9 +615,8 @@ class _FaiSettingsDialogState extends State { List _securityPanel(AppLocalizations l, ThemeData theme) { return [ _panelTitle( - 'Security & credentials', - 'Tokens the hub holds on the operator\'s behalf. Stored in ' - '~/.fai/ with mode 0600; never sent in telemetry.', + l.settingsSecurityPanelTitle, + l.settingsSecurityPanelBody, theme, ), _RegistryCredentialsPanel( @@ -640,9 +636,8 @@ class _FaiSettingsDialogState extends State { List _maintenancePanel(AppLocalizations l, ThemeData theme) { return [ _panelTitle( - 'Maintenance', - 'Destructive operations gated behind explicit confirm ' - 'dialogs. Use when re-pilot-testing or debugging.', + l.settingsMaintenancePanelTitle, + l.settingsMaintenancePanelBody, theme, ), _MaintenancePanel( @@ -2525,19 +2520,19 @@ class _ScopeRow extends StatelessWidget { ), IconButton( icon: const Icon(Icons.arrow_upward, size: 16), - tooltip: 'Move up', + tooltip: AppLocalizations.of(context)!.tooltipMoveUp, onPressed: busy || index == 0 ? null : onUp, visualDensity: VisualDensity.compact, ), IconButton( icon: const Icon(Icons.arrow_downward, size: 16), - tooltip: 'Move down', + tooltip: AppLocalizations.of(context)!.tooltipMoveDown, onPressed: busy || index == total - 1 ? null : onDown, visualDensity: VisualDensity.compact, ), IconButton( icon: const Icon(Icons.delete_outline, size: 16), - tooltip: 'Remove', + tooltip: AppLocalizations.of(context)!.tooltipRemove, onPressed: busy ? null : onRemove, visualDensity: VisualDensity.compact, ), @@ -2559,13 +2554,26 @@ class _Sidebar extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; final items = <(_Category, IconData, String)>[ - (_Category.general, Icons.tune, 'General'), - (_Category.appearance, Icons.palette_outlined, 'Appearance'), - (_Category.ai, Icons.psychology_outlined, 'System AI'), - (_Category.integrations, Icons.hub_outlined, 'Integrations'), - (_Category.security, Icons.shield_outlined, 'Security'), - (_Category.maintenance, Icons.build_outlined, 'Maintenance'), + (_Category.general, Icons.tune, l.settingsCategoryGeneral), + ( + _Category.appearance, + Icons.palette_outlined, + l.settingsCategoryAppearance, + ), + (_Category.ai, Icons.psychology_outlined, l.settingsCategoryAi), + ( + _Category.integrations, + Icons.hub_outlined, + l.settingsCategoryIntegrations, + ), + (_Category.security, Icons.shield_outlined, l.settingsCategorySecurity), + ( + _Category.maintenance, + Icons.build_outlined, + l.settingsCategoryMaintenance, + ), ]; return SizedBox( width: 220, diff --git a/pubspec.lock b/pubspec.lock index aecae24..029963f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -126,7 +126,7 @@ packages: path: "../fai_studio_flow_editor" relative: true source: path - version: "0.19.0" + version: "0.20.0" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d96f714..6dad0ed 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.66.0 +version: 0.67.0 environment: sdk: ^3.11.0-200.1.beta