feat(studio): Doctor + Modules localized + sparse-store federation nudge (v0.25.0)
Two threads.
Translation expansion: every visible string on the Doctor
page (section headers, status text, button labels, daemon-
control card, daemon-files panel, update banner) and the
Modules page (recent-activity strip, capabilities label,
uninstall dialog + toast) flips between DE and EN with the
sidebar toggle. Adds ~50 ARB keys split between
`app_en.arb` / `app_de.arb`. Pluralised + parametrised
strings (`{n} events verified`, `Running on {name}: {endpoint}`)
use the standard ICU placeholder syntax so future locales
slot in without code changes.
Sparse-store federation nudge: the Store page renders an
inline banner above the grid when the operator has zero
federated entries — single biggest store-fullness lever is
configuring an MCP server / n8n endpoint, so the banner
says exactly that and the button opens Settings straight
to the editor. Banner dismisses automatically the next
render after a federated entry appears.
Coverage stand for translation: Doctor + Modules + sidebar
+ page titles + nav + Cmd+K labels are bilingual. Settings
dialog, Approvals cards, Audit drilldown, MCP/n8n editors,
Store search hint + filter chips remain English-literal.
The infrastructure (ARB plumbing, generator hookup,
locale-notifier) means each follow-up surface is a
one-line ARB edit + one call-site swap.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
ee83eb851a
commit
349619d68e
11 changed files with 1091 additions and 80 deletions
|
|
@ -61,6 +61,9 @@
|
|||
"@storeNResults": {
|
||||
"placeholders": { "n": { "type": "int" } }
|
||||
},
|
||||
"storeFederationNudgeTitle": "Mehr Capabilities?",
|
||||
"storeFederationNudgeBody": "MCP-Server oder n8n-Endpunkt unter Einstellungen → MCP-Clients / N8N-Endpunkte konfigurieren. Entdeckte Tools und Workflows tauchen hier als `mcp.<server>.<tool>`- und `n8n.<endpoint>.<workflow>`-Capabilities auf — jeder Anthropic-/Community-MCP-Server bringt mehrere auf einmal.",
|
||||
"storeFederationNudgeButton": "Einstellungen öffnen",
|
||||
|
||||
"auditTitle": "Protokoll",
|
||||
"auditNoEvents": "Noch keine Ereignisse",
|
||||
|
|
@ -68,7 +71,32 @@
|
|||
|
||||
"modulesTitle": "Module",
|
||||
"modulesNoneTitle": "Noch keine Module",
|
||||
"modulesNoneHint": "`fai install <capability-name>` ausführen oder ~/.fai/modules/ prüfen.",
|
||||
"modulesReloadTooltip": "Aktualisieren",
|
||||
"modulesRecentActivity": "LETZTE AKTIVITÄT",
|
||||
"modulesRecentActivityHint": "letzte {n} install/uninstall-Ereignisse aus dem Audit-Log",
|
||||
"@modulesRecentActivityHint": { "placeholders": { "n": { "type": "int" } } },
|
||||
"modulesActivityInstalled": "installiert",
|
||||
"modulesActivityUninstalled": "deinstalliert",
|
||||
"modulesCapabilitiesLabel": "Capabilities",
|
||||
"modulesUninstallTitle": "Modul deinstallieren?",
|
||||
"modulesUninstallBody": "Entfernt {name} v{version} von diesem Hub. Flows, die es verwenden, schlagen beim nächsten Lauf fehl. Ein `module.uninstalled`-Audit-Event wird festgehalten.",
|
||||
"@modulesUninstallBody": {
|
||||
"placeholders": {
|
||||
"name": { "type": "String" },
|
||||
"version": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"modulesUninstalledToast": "{name} v{version} deinstalliert.",
|
||||
"@modulesUninstalledToast": {
|
||||
"placeholders": {
|
||||
"name": { "type": "String" },
|
||||
"version": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"modulesUninstallFailed": "Deinstallation fehlgeschlagen: {error}",
|
||||
"@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||
"modulesUninstalling": "Wird deinstalliert…",
|
||||
|
||||
"approvalsTitle": "Freigaben",
|
||||
"approvalsTabPending": "Offen",
|
||||
|
|
@ -77,6 +105,73 @@
|
|||
"approvalsInboxHint": "Alles erledigt. Neue `system.approval@^0`-Schritte landen automatisch hier.",
|
||||
|
||||
"doctorTitle": "Diagnose",
|
||||
"doctorRecheckTooltip": "Erneut prüfen",
|
||||
"doctorEventLogSection": "Audit-Log",
|
||||
"doctorModulesApprovalsSection": "Module & Freigaben",
|
||||
"doctorHostServicesSection": "Host-Dienste",
|
||||
"doctorDaemonFilesSection": "Daemon-Dateien",
|
||||
"doctorDaemonControlSection": "Daemon-Steuerung",
|
||||
"doctorChainIntact": "Hash-Chain intakt",
|
||||
"doctorChainIntactDetail": "{n} Ereignisse · prev_event_sha256 lückenlos verifiziert",
|
||||
"@doctorChainIntactDetail": { "placeholders": { "n": { "type": "int" } } },
|
||||
"doctorChainTampered": "Manipulation erkannt",
|
||||
"doctorChainTamperedDetail": "{verified} von {total} verifiziert vor Mismatch bei {id}",
|
||||
"@doctorChainTamperedDetail": {
|
||||
"placeholders": {
|
||||
"verified": { "type": "int" },
|
||||
"total": { "type": "int" },
|
||||
"id": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"doctorChainPillOk": "WORM-1",
|
||||
"doctorChainPillTamper": "MANIPULIERT",
|
||||
"doctorVerifyNow": "Jetzt prüfen",
|
||||
"doctorRestart": "Neustart",
|
||||
"doctorStart": "Starten",
|
||||
"doctorStop": "Stoppen",
|
||||
"doctorStatusAction": "Status",
|
||||
"doctorRunningOn": "Läuft auf {name}: {endpoint}",
|
||||
"@doctorRunningOn": {
|
||||
"placeholders": {
|
||||
"name": { "type": "String" },
|
||||
"endpoint": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"doctorStoppedOn": "{name}-Daemon gestoppt",
|
||||
"@doctorStoppedOn": { "placeholders": { "name": { "type": "String" } } },
|
||||
"doctorStatusUnknown": "Daemon-Status: …",
|
||||
"doctorPillRunning": "läuft",
|
||||
"doctorPillStopped": "gestoppt",
|
||||
"doctorDaemonControlHint": "Studio ruft das Platform-Binary auf — entspricht `fai daemon …`, sodass CLI und UI im Gleichschritt bleiben.",
|
||||
"doctorDaemonControlWorking": "Wird ausgeführt…",
|
||||
"doctorPathLog": "Log",
|
||||
"doctorPathConfig": "Konfig",
|
||||
"doctorPathDb": "Audit-DB",
|
||||
"doctorPathModules": "Module-Verzeichnis",
|
||||
"doctorPathFlows": "Flow-Verzeichnis",
|
||||
"doctorPathPid": "PID-Datei",
|
||||
"doctorPathsEmpty": "Daemon hat keine Datei-Pfade gemeldet. Hub aktualisieren via `fai daemon restart`.",
|
||||
"doctorOpenError": "Konnte nicht öffnen: {detail}",
|
||||
"@doctorOpenError": { "placeholders": { "detail": { "type": "String" } } },
|
||||
"doctorUpdateAvailable": "Update verfügbar — {version}",
|
||||
"@doctorUpdateAvailable": { "placeholders": { "version": { "type": "String" } } },
|
||||
"doctorUpdateBody": "Kanal {channel} bietet jetzt {version}. Per Button anwenden — Studio ruft `fai update apply --channel {channel}` auf.",
|
||||
"@doctorUpdateBody": {
|
||||
"placeholders": {
|
||||
"channel": { "type": "String" },
|
||||
"version": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"doctorUpdateUnreachable": "Release-Host nicht erreichbar",
|
||||
"doctorUpdateUnreachableBody": "Release-Feed für {channel} konnte nicht erreicht werden.",
|
||||
"@doctorUpdateUnreachableBody": { "placeholders": { "channel": { "type": "String" } } },
|
||||
"doctorReleaseNotes": "Notes: {url}",
|
||||
"@doctorReleaseNotes": { "placeholders": { "url": { "type": "String" } } },
|
||||
"doctorPillNew": "neu",
|
||||
"doctorPillOffline": "offline",
|
||||
"doctorApplyUpdate": "Update anwenden",
|
||||
"doctorApplying": "Wird angewendet…",
|
||||
"doctorApplyDone": "Update angewendet. Studio neu starten, um die neue Daemon-Version zu sehen.",
|
||||
|
||||
"mcpHeader": "MCP-CLIENTS",
|
||||
"mcpHint": "Externe MCP-Server bringen ihre Tools als `mcp.<server>.<tool>` Capabilities ein. Einmal konfigurieren, im Store sichtbar.",
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@
|
|||
"@storeNResults": {
|
||||
"placeholders": { "n": { "type": "int" } }
|
||||
},
|
||||
"storeFederationNudgeTitle": "Want more capabilities?",
|
||||
"storeFederationNudgeBody": "Configure an MCP server or n8n endpoint in Settings → MCP Clients / N8N Endpoints. Discovered tools and workflows surface here as `mcp.<server>.<tool>` and `n8n.<endpoint>.<workflow>` capabilities — every Anthropic / community MCP server adds a handful at once.",
|
||||
"storeFederationNudgeButton": "Open Settings",
|
||||
|
||||
"auditTitle": "Audit",
|
||||
"auditNoEvents": "No events yet",
|
||||
|
|
@ -69,7 +72,32 @@
|
|||
|
||||
"modulesTitle": "Modules",
|
||||
"modulesNoneTitle": "No modules yet",
|
||||
"modulesNoneHint": "Run `fai install <capability-name>` or check ~/.fai/modules/.",
|
||||
"modulesReloadTooltip": "Reload",
|
||||
"modulesRecentActivity": "RECENT ACTIVITY",
|
||||
"modulesRecentActivityHint": "last {n} install/uninstall events from the audit log",
|
||||
"@modulesRecentActivityHint": { "placeholders": { "n": { "type": "int" } } },
|
||||
"modulesActivityInstalled": "installed",
|
||||
"modulesActivityUninstalled": "uninstalled",
|
||||
"modulesCapabilitiesLabel": "Capabilities",
|
||||
"modulesUninstallTitle": "Uninstall module?",
|
||||
"modulesUninstallBody": "Removes {name} v{version} from this hub. Flows that reference it will fail at the next run. A `module.uninstalled` audit event is recorded.",
|
||||
"@modulesUninstallBody": {
|
||||
"placeholders": {
|
||||
"name": { "type": "String" },
|
||||
"version": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"modulesUninstalledToast": "Uninstalled {name} v{version}.",
|
||||
"@modulesUninstalledToast": {
|
||||
"placeholders": {
|
||||
"name": { "type": "String" },
|
||||
"version": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"modulesUninstallFailed": "Uninstall failed: {error}",
|
||||
"@modulesUninstallFailed": { "placeholders": { "error": { "type": "String" } } },
|
||||
"modulesUninstalling": "Uninstalling…",
|
||||
|
||||
"approvalsTitle": "Approvals",
|
||||
"approvalsTabPending": "Pending",
|
||||
|
|
@ -78,6 +106,73 @@
|
|||
"approvalsInboxHint": "All caught up. New `system.approval@^0` steps land here automatically.",
|
||||
|
||||
"doctorTitle": "Doctor",
|
||||
"doctorRecheckTooltip": "Re-check",
|
||||
"doctorEventLogSection": "Event log",
|
||||
"doctorModulesApprovalsSection": "Modules & approvals",
|
||||
"doctorHostServicesSection": "Host services",
|
||||
"doctorDaemonFilesSection": "Daemon files",
|
||||
"doctorDaemonControlSection": "Daemon control",
|
||||
"doctorChainIntact": "Hash chain intact",
|
||||
"doctorChainIntactDetail": "{n} events · prev_event_sha256 verified end-to-end",
|
||||
"@doctorChainIntactDetail": { "placeholders": { "n": { "type": "int" } } },
|
||||
"doctorChainTampered": "Tampering detected",
|
||||
"doctorChainTamperedDetail": "{verified} of {total} verified before mismatch at {id}",
|
||||
"@doctorChainTamperedDetail": {
|
||||
"placeholders": {
|
||||
"verified": { "type": "int" },
|
||||
"total": { "type": "int" },
|
||||
"id": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"doctorChainPillOk": "WORM-1",
|
||||
"doctorChainPillTamper": "TAMPER",
|
||||
"doctorVerifyNow": "Verify now",
|
||||
"doctorRestart": "Restart",
|
||||
"doctorStart": "Start",
|
||||
"doctorStop": "Stop",
|
||||
"doctorStatusAction": "Status",
|
||||
"doctorRunningOn": "Running on {name}: {endpoint}",
|
||||
"@doctorRunningOn": {
|
||||
"placeholders": {
|
||||
"name": { "type": "String" },
|
||||
"endpoint": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"doctorStoppedOn": "{name} daemon stopped",
|
||||
"@doctorStoppedOn": { "placeholders": { "name": { "type": "String" } } },
|
||||
"doctorStatusUnknown": "Daemon status: …",
|
||||
"doctorPillRunning": "running",
|
||||
"doctorPillStopped": "stopped",
|
||||
"doctorDaemonControlHint": "Studio shells out to the platform binary — mirrors `fai daemon …` exactly so the CLI and UI stay in lockstep.",
|
||||
"doctorDaemonControlWorking": "Working…",
|
||||
"doctorPathLog": "Log",
|
||||
"doctorPathConfig": "Config",
|
||||
"doctorPathDb": "Audit DB",
|
||||
"doctorPathModules": "Modules dir",
|
||||
"doctorPathFlows": "Flows dir",
|
||||
"doctorPathPid": "PID file",
|
||||
"doctorPathsEmpty": "Daemon did not report file paths. Update the running hub via `fai daemon restart`.",
|
||||
"doctorOpenError": "Could not open: {detail}",
|
||||
"@doctorOpenError": { "placeholders": { "detail": { "type": "String" } } },
|
||||
"doctorUpdateAvailable": "Update available — {version}",
|
||||
"@doctorUpdateAvailable": { "placeholders": { "version": { "type": "String" } } },
|
||||
"doctorUpdateBody": "Channel {channel} now offers {version}. Apply via the button — Studio shells out to `fai update apply --channel {channel}`.",
|
||||
"@doctorUpdateBody": {
|
||||
"placeholders": {
|
||||
"channel": { "type": "String" },
|
||||
"version": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"doctorUpdateUnreachable": "Release host unreachable",
|
||||
"doctorUpdateUnreachableBody": "Could not contact the release feed for {channel}.",
|
||||
"@doctorUpdateUnreachableBody": { "placeholders": { "channel": { "type": "String" } } },
|
||||
"doctorReleaseNotes": "notes: {url}",
|
||||
"@doctorReleaseNotes": { "placeholders": { "url": { "type": "String" } } },
|
||||
"doctorPillNew": "new",
|
||||
"doctorPillOffline": "offline",
|
||||
"doctorApplyUpdate": "Apply update",
|
||||
"doctorApplying": "Applying…",
|
||||
"doctorApplyDone": "Update applied. Restart Studio to see the new daemon version.",
|
||||
|
||||
"mcpHeader": "MCP CLIENTS",
|
||||
"mcpHint": "External MCP servers federate their tools as `mcp.<server>.<tool>` capabilities. Configure once, see them in the Store.",
|
||||
|
|
|
|||
|
|
@ -404,6 +404,24 @@ abstract class AppLocalizations {
|
|||
/// **'{n} result{n, plural, =1{} other{s}}'**
|
||||
String storeNResults(int n);
|
||||
|
||||
/// No description provided for @storeFederationNudgeTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Want more capabilities?'**
|
||||
String get storeFederationNudgeTitle;
|
||||
|
||||
/// No description provided for @storeFederationNudgeBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Configure an MCP server or n8n endpoint in Settings → MCP Clients / N8N Endpoints. Discovered tools and workflows surface here as `mcp.<server>.<tool>` and `n8n.<endpoint>.<workflow>` capabilities — every Anthropic / community MCP server adds a handful at once.'**
|
||||
String get storeFederationNudgeBody;
|
||||
|
||||
/// No description provided for @storeFederationNudgeButton.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open Settings'**
|
||||
String get storeFederationNudgeButton;
|
||||
|
||||
/// No description provided for @auditTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -434,12 +452,78 @@ abstract class AppLocalizations {
|
|||
/// **'No modules yet'**
|
||||
String get modulesNoneTitle;
|
||||
|
||||
/// No description provided for @modulesNoneHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Run `fai install <capability-name>` or check ~/.fai/modules/.'**
|
||||
String get modulesNoneHint;
|
||||
|
||||
/// No description provided for @modulesReloadTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reload'**
|
||||
String get modulesReloadTooltip;
|
||||
|
||||
/// No description provided for @modulesRecentActivity.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'RECENT ACTIVITY'**
|
||||
String get modulesRecentActivity;
|
||||
|
||||
/// No description provided for @modulesRecentActivityHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'last {n} install/uninstall events from the audit log'**
|
||||
String modulesRecentActivityHint(int n);
|
||||
|
||||
/// No description provided for @modulesActivityInstalled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'installed'**
|
||||
String get modulesActivityInstalled;
|
||||
|
||||
/// No description provided for @modulesActivityUninstalled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'uninstalled'**
|
||||
String get modulesActivityUninstalled;
|
||||
|
||||
/// No description provided for @modulesCapabilitiesLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Capabilities'**
|
||||
String get modulesCapabilitiesLabel;
|
||||
|
||||
/// No description provided for @modulesUninstallTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Uninstall module?'**
|
||||
String get modulesUninstallTitle;
|
||||
|
||||
/// No description provided for @modulesUninstallBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Removes {name} v{version} from this hub. Flows that reference it will fail at the next run. A `module.uninstalled` audit event is recorded.'**
|
||||
String modulesUninstallBody(String name, String version);
|
||||
|
||||
/// No description provided for @modulesUninstalledToast.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Uninstalled {name} v{version}.'**
|
||||
String modulesUninstalledToast(String name, String version);
|
||||
|
||||
/// No description provided for @modulesUninstallFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Uninstall failed: {error}'**
|
||||
String modulesUninstallFailed(String error);
|
||||
|
||||
/// No description provided for @modulesUninstalling.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Uninstalling…'**
|
||||
String get modulesUninstalling;
|
||||
|
||||
/// No description provided for @approvalsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -476,6 +560,258 @@ abstract class AppLocalizations {
|
|||
/// **'Doctor'**
|
||||
String get doctorTitle;
|
||||
|
||||
/// No description provided for @doctorRecheckTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Re-check'**
|
||||
String get doctorRecheckTooltip;
|
||||
|
||||
/// No description provided for @doctorEventLogSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Event log'**
|
||||
String get doctorEventLogSection;
|
||||
|
||||
/// No description provided for @doctorModulesApprovalsSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Modules & approvals'**
|
||||
String get doctorModulesApprovalsSection;
|
||||
|
||||
/// No description provided for @doctorHostServicesSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Host services'**
|
||||
String get doctorHostServicesSection;
|
||||
|
||||
/// No description provided for @doctorDaemonFilesSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Daemon files'**
|
||||
String get doctorDaemonFilesSection;
|
||||
|
||||
/// No description provided for @doctorDaemonControlSection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Daemon control'**
|
||||
String get doctorDaemonControlSection;
|
||||
|
||||
/// No description provided for @doctorChainIntact.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hash chain intact'**
|
||||
String get doctorChainIntact;
|
||||
|
||||
/// No description provided for @doctorChainIntactDetail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{n} events · prev_event_sha256 verified end-to-end'**
|
||||
String doctorChainIntactDetail(int n);
|
||||
|
||||
/// No description provided for @doctorChainTampered.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tampering detected'**
|
||||
String get doctorChainTampered;
|
||||
|
||||
/// No description provided for @doctorChainTamperedDetail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{verified} of {total} verified before mismatch at {id}'**
|
||||
String doctorChainTamperedDetail(int verified, int total, String id);
|
||||
|
||||
/// No description provided for @doctorChainPillOk.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'WORM-1'**
|
||||
String get doctorChainPillOk;
|
||||
|
||||
/// No description provided for @doctorChainPillTamper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'TAMPER'**
|
||||
String get doctorChainPillTamper;
|
||||
|
||||
/// No description provided for @doctorVerifyNow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Verify now'**
|
||||
String get doctorVerifyNow;
|
||||
|
||||
/// No description provided for @doctorRestart.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Restart'**
|
||||
String get doctorRestart;
|
||||
|
||||
/// No description provided for @doctorStart.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start'**
|
||||
String get doctorStart;
|
||||
|
||||
/// No description provided for @doctorStop.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stop'**
|
||||
String get doctorStop;
|
||||
|
||||
/// No description provided for @doctorStatusAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Status'**
|
||||
String get doctorStatusAction;
|
||||
|
||||
/// No description provided for @doctorRunningOn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Running on {name}: {endpoint}'**
|
||||
String doctorRunningOn(String name, String endpoint);
|
||||
|
||||
/// No description provided for @doctorStoppedOn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{name} daemon stopped'**
|
||||
String doctorStoppedOn(String name);
|
||||
|
||||
/// No description provided for @doctorStatusUnknown.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Daemon status: …'**
|
||||
String get doctorStatusUnknown;
|
||||
|
||||
/// No description provided for @doctorPillRunning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'running'**
|
||||
String get doctorPillRunning;
|
||||
|
||||
/// No description provided for @doctorPillStopped.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'stopped'**
|
||||
String get doctorPillStopped;
|
||||
|
||||
/// No description provided for @doctorDaemonControlHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Studio shells out to the platform binary — mirrors `fai daemon …` exactly so the CLI and UI stay in lockstep.'**
|
||||
String get doctorDaemonControlHint;
|
||||
|
||||
/// No description provided for @doctorDaemonControlWorking.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Working…'**
|
||||
String get doctorDaemonControlWorking;
|
||||
|
||||
/// No description provided for @doctorPathLog.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Log'**
|
||||
String get doctorPathLog;
|
||||
|
||||
/// No description provided for @doctorPathConfig.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Config'**
|
||||
String get doctorPathConfig;
|
||||
|
||||
/// No description provided for @doctorPathDb.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Audit DB'**
|
||||
String get doctorPathDb;
|
||||
|
||||
/// No description provided for @doctorPathModules.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Modules dir'**
|
||||
String get doctorPathModules;
|
||||
|
||||
/// No description provided for @doctorPathFlows.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Flows dir'**
|
||||
String get doctorPathFlows;
|
||||
|
||||
/// No description provided for @doctorPathPid.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'PID file'**
|
||||
String get doctorPathPid;
|
||||
|
||||
/// No description provided for @doctorPathsEmpty.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Daemon did not report file paths. Update the running hub via `fai daemon restart`.'**
|
||||
String get doctorPathsEmpty;
|
||||
|
||||
/// No description provided for @doctorOpenError.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Could not open: {detail}'**
|
||||
String doctorOpenError(String detail);
|
||||
|
||||
/// No description provided for @doctorUpdateAvailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Update available — {version}'**
|
||||
String doctorUpdateAvailable(String version);
|
||||
|
||||
/// No description provided for @doctorUpdateBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Channel {channel} now offers {version}. Apply via the button — Studio shells out to `fai update apply --channel {channel}`.'**
|
||||
String doctorUpdateBody(String channel, String version);
|
||||
|
||||
/// No description provided for @doctorUpdateUnreachable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Release host unreachable'**
|
||||
String get doctorUpdateUnreachable;
|
||||
|
||||
/// No description provided for @doctorUpdateUnreachableBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Could not contact the release feed for {channel}.'**
|
||||
String doctorUpdateUnreachableBody(String channel);
|
||||
|
||||
/// No description provided for @doctorReleaseNotes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'notes: {url}'**
|
||||
String doctorReleaseNotes(String url);
|
||||
|
||||
/// No description provided for @doctorPillNew.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'new'**
|
||||
String get doctorPillNew;
|
||||
|
||||
/// No description provided for @doctorPillOffline.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'offline'**
|
||||
String get doctorPillOffline;
|
||||
|
||||
/// No description provided for @doctorApplyUpdate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Apply update'**
|
||||
String get doctorApplyUpdate;
|
||||
|
||||
/// No description provided for @doctorApplying.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Applying…'**
|
||||
String get doctorApplying;
|
||||
|
||||
/// No description provided for @doctorApplyDone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Update applied. Restart Studio to see the new daemon version.'**
|
||||
String get doctorApplyDone;
|
||||
|
||||
/// No description provided for @mcpHeader.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -166,6 +166,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
return '$n Treffer';
|
||||
}
|
||||
|
||||
@override
|
||||
String get storeFederationNudgeTitle => 'Mehr Capabilities?';
|
||||
|
||||
@override
|
||||
String get storeFederationNudgeBody =>
|
||||
'MCP-Server oder n8n-Endpunkt unter Einstellungen → MCP-Clients / N8N-Endpunkte konfigurieren. Entdeckte Tools und Workflows tauchen hier als `mcp.<server>.<tool>`- und `n8n.<endpoint>.<workflow>`-Capabilities auf — jeder Anthropic-/Community-MCP-Server bringt mehrere auf einmal.';
|
||||
|
||||
@override
|
||||
String get storeFederationNudgeButton => 'Einstellungen öffnen';
|
||||
|
||||
@override
|
||||
String get auditTitle => 'Protokoll';
|
||||
|
||||
|
|
@ -182,9 +192,51 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get modulesNoneTitle => 'Noch keine Module';
|
||||
|
||||
@override
|
||||
String get modulesNoneHint =>
|
||||
'`fai install <capability-name>` ausführen oder ~/.fai/modules/ prüfen.';
|
||||
|
||||
@override
|
||||
String get modulesReloadTooltip => 'Aktualisieren';
|
||||
|
||||
@override
|
||||
String get modulesRecentActivity => 'LETZTE AKTIVITÄT';
|
||||
|
||||
@override
|
||||
String modulesRecentActivityHint(int n) {
|
||||
return 'letzte $n install/uninstall-Ereignisse aus dem Audit-Log';
|
||||
}
|
||||
|
||||
@override
|
||||
String get modulesActivityInstalled => 'installiert';
|
||||
|
||||
@override
|
||||
String get modulesActivityUninstalled => 'deinstalliert';
|
||||
|
||||
@override
|
||||
String get modulesCapabilitiesLabel => 'Capabilities';
|
||||
|
||||
@override
|
||||
String get modulesUninstallTitle => 'Modul deinstallieren?';
|
||||
|
||||
@override
|
||||
String modulesUninstallBody(String name, String version) {
|
||||
return 'Entfernt $name v$version von diesem Hub. Flows, die es verwenden, schlagen beim nächsten Lauf fehl. Ein `module.uninstalled`-Audit-Event wird festgehalten.';
|
||||
}
|
||||
|
||||
@override
|
||||
String modulesUninstalledToast(String name, String version) {
|
||||
return '$name v$version deinstalliert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String modulesUninstallFailed(String error) {
|
||||
return 'Deinstallation fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get modulesUninstalling => 'Wird deinstalliert…';
|
||||
|
||||
@override
|
||||
String get approvalsTitle => 'Freigaben';
|
||||
|
||||
|
|
@ -204,6 +256,153 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get doctorTitle => 'Diagnose';
|
||||
|
||||
@override
|
||||
String get doctorRecheckTooltip => 'Erneut prüfen';
|
||||
|
||||
@override
|
||||
String get doctorEventLogSection => 'Audit-Log';
|
||||
|
||||
@override
|
||||
String get doctorModulesApprovalsSection => 'Module & Freigaben';
|
||||
|
||||
@override
|
||||
String get doctorHostServicesSection => 'Host-Dienste';
|
||||
|
||||
@override
|
||||
String get doctorDaemonFilesSection => 'Daemon-Dateien';
|
||||
|
||||
@override
|
||||
String get doctorDaemonControlSection => 'Daemon-Steuerung';
|
||||
|
||||
@override
|
||||
String get doctorChainIntact => 'Hash-Chain intakt';
|
||||
|
||||
@override
|
||||
String doctorChainIntactDetail(int n) {
|
||||
return '$n Ereignisse · prev_event_sha256 lückenlos verifiziert';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorChainTampered => 'Manipulation erkannt';
|
||||
|
||||
@override
|
||||
String doctorChainTamperedDetail(int verified, int total, String id) {
|
||||
return '$verified von $total verifiziert vor Mismatch bei $id';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorChainPillOk => 'WORM-1';
|
||||
|
||||
@override
|
||||
String get doctorChainPillTamper => 'MANIPULIERT';
|
||||
|
||||
@override
|
||||
String get doctorVerifyNow => 'Jetzt prüfen';
|
||||
|
||||
@override
|
||||
String get doctorRestart => 'Neustart';
|
||||
|
||||
@override
|
||||
String get doctorStart => 'Starten';
|
||||
|
||||
@override
|
||||
String get doctorStop => 'Stoppen';
|
||||
|
||||
@override
|
||||
String get doctorStatusAction => 'Status';
|
||||
|
||||
@override
|
||||
String doctorRunningOn(String name, String endpoint) {
|
||||
return 'Läuft auf $name: $endpoint';
|
||||
}
|
||||
|
||||
@override
|
||||
String doctorStoppedOn(String name) {
|
||||
return '$name-Daemon gestoppt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorStatusUnknown => 'Daemon-Status: …';
|
||||
|
||||
@override
|
||||
String get doctorPillRunning => 'läuft';
|
||||
|
||||
@override
|
||||
String get doctorPillStopped => 'gestoppt';
|
||||
|
||||
@override
|
||||
String get doctorDaemonControlHint =>
|
||||
'Studio ruft das Platform-Binary auf — entspricht `fai daemon …`, sodass CLI und UI im Gleichschritt bleiben.';
|
||||
|
||||
@override
|
||||
String get doctorDaemonControlWorking => 'Wird ausgeführt…';
|
||||
|
||||
@override
|
||||
String get doctorPathLog => 'Log';
|
||||
|
||||
@override
|
||||
String get doctorPathConfig => 'Konfig';
|
||||
|
||||
@override
|
||||
String get doctorPathDb => 'Audit-DB';
|
||||
|
||||
@override
|
||||
String get doctorPathModules => 'Module-Verzeichnis';
|
||||
|
||||
@override
|
||||
String get doctorPathFlows => 'Flow-Verzeichnis';
|
||||
|
||||
@override
|
||||
String get doctorPathPid => 'PID-Datei';
|
||||
|
||||
@override
|
||||
String get doctorPathsEmpty =>
|
||||
'Daemon hat keine Datei-Pfade gemeldet. Hub aktualisieren via `fai daemon restart`.';
|
||||
|
||||
@override
|
||||
String doctorOpenError(String detail) {
|
||||
return 'Konnte nicht öffnen: $detail';
|
||||
}
|
||||
|
||||
@override
|
||||
String doctorUpdateAvailable(String version) {
|
||||
return 'Update verfügbar — $version';
|
||||
}
|
||||
|
||||
@override
|
||||
String doctorUpdateBody(String channel, String version) {
|
||||
return 'Kanal $channel bietet jetzt $version. Per Button anwenden — Studio ruft `fai update apply --channel $channel` auf.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorUpdateUnreachable => 'Release-Host nicht erreichbar';
|
||||
|
||||
@override
|
||||
String doctorUpdateUnreachableBody(String channel) {
|
||||
return 'Release-Feed für $channel konnte nicht erreicht werden.';
|
||||
}
|
||||
|
||||
@override
|
||||
String doctorReleaseNotes(String url) {
|
||||
return 'Notes: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorPillNew => 'neu';
|
||||
|
||||
@override
|
||||
String get doctorPillOffline => 'offline';
|
||||
|
||||
@override
|
||||
String get doctorApplyUpdate => 'Update anwenden';
|
||||
|
||||
@override
|
||||
String get doctorApplying => 'Wird angewendet…';
|
||||
|
||||
@override
|
||||
String get doctorApplyDone =>
|
||||
'Update angewendet. Studio neu starten, um die neue Daemon-Version zu sehen.';
|
||||
|
||||
@override
|
||||
String get mcpHeader => 'MCP-CLIENTS';
|
||||
|
||||
|
|
|
|||
|
|
@ -172,6 +172,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
return '$n result$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get storeFederationNudgeTitle => 'Want more capabilities?';
|
||||
|
||||
@override
|
||||
String get storeFederationNudgeBody =>
|
||||
'Configure an MCP server or n8n endpoint in Settings → MCP Clients / N8N Endpoints. Discovered tools and workflows surface here as `mcp.<server>.<tool>` and `n8n.<endpoint>.<workflow>` capabilities — every Anthropic / community MCP server adds a handful at once.';
|
||||
|
||||
@override
|
||||
String get storeFederationNudgeButton => 'Open Settings';
|
||||
|
||||
@override
|
||||
String get auditTitle => 'Audit';
|
||||
|
||||
|
|
@ -188,9 +198,51 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get modulesNoneTitle => 'No modules yet';
|
||||
|
||||
@override
|
||||
String get modulesNoneHint =>
|
||||
'Run `fai install <capability-name>` or check ~/.fai/modules/.';
|
||||
|
||||
@override
|
||||
String get modulesReloadTooltip => 'Reload';
|
||||
|
||||
@override
|
||||
String get modulesRecentActivity => 'RECENT ACTIVITY';
|
||||
|
||||
@override
|
||||
String modulesRecentActivityHint(int n) {
|
||||
return 'last $n install/uninstall events from the audit log';
|
||||
}
|
||||
|
||||
@override
|
||||
String get modulesActivityInstalled => 'installed';
|
||||
|
||||
@override
|
||||
String get modulesActivityUninstalled => 'uninstalled';
|
||||
|
||||
@override
|
||||
String get modulesCapabilitiesLabel => 'Capabilities';
|
||||
|
||||
@override
|
||||
String get modulesUninstallTitle => 'Uninstall module?';
|
||||
|
||||
@override
|
||||
String modulesUninstallBody(String name, String version) {
|
||||
return 'Removes $name v$version from this hub. Flows that reference it will fail at the next run. A `module.uninstalled` audit event is recorded.';
|
||||
}
|
||||
|
||||
@override
|
||||
String modulesUninstalledToast(String name, String version) {
|
||||
return 'Uninstalled $name v$version.';
|
||||
}
|
||||
|
||||
@override
|
||||
String modulesUninstallFailed(String error) {
|
||||
return 'Uninstall failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get modulesUninstalling => 'Uninstalling…';
|
||||
|
||||
@override
|
||||
String get approvalsTitle => 'Approvals';
|
||||
|
||||
|
|
@ -210,6 +262,153 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get doctorTitle => 'Doctor';
|
||||
|
||||
@override
|
||||
String get doctorRecheckTooltip => 'Re-check';
|
||||
|
||||
@override
|
||||
String get doctorEventLogSection => 'Event log';
|
||||
|
||||
@override
|
||||
String get doctorModulesApprovalsSection => 'Modules & approvals';
|
||||
|
||||
@override
|
||||
String get doctorHostServicesSection => 'Host services';
|
||||
|
||||
@override
|
||||
String get doctorDaemonFilesSection => 'Daemon files';
|
||||
|
||||
@override
|
||||
String get doctorDaemonControlSection => 'Daemon control';
|
||||
|
||||
@override
|
||||
String get doctorChainIntact => 'Hash chain intact';
|
||||
|
||||
@override
|
||||
String doctorChainIntactDetail(int n) {
|
||||
return '$n events · prev_event_sha256 verified end-to-end';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorChainTampered => 'Tampering detected';
|
||||
|
||||
@override
|
||||
String doctorChainTamperedDetail(int verified, int total, String id) {
|
||||
return '$verified of $total verified before mismatch at $id';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorChainPillOk => 'WORM-1';
|
||||
|
||||
@override
|
||||
String get doctorChainPillTamper => 'TAMPER';
|
||||
|
||||
@override
|
||||
String get doctorVerifyNow => 'Verify now';
|
||||
|
||||
@override
|
||||
String get doctorRestart => 'Restart';
|
||||
|
||||
@override
|
||||
String get doctorStart => 'Start';
|
||||
|
||||
@override
|
||||
String get doctorStop => 'Stop';
|
||||
|
||||
@override
|
||||
String get doctorStatusAction => 'Status';
|
||||
|
||||
@override
|
||||
String doctorRunningOn(String name, String endpoint) {
|
||||
return 'Running on $name: $endpoint';
|
||||
}
|
||||
|
||||
@override
|
||||
String doctorStoppedOn(String name) {
|
||||
return '$name daemon stopped';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorStatusUnknown => 'Daemon status: …';
|
||||
|
||||
@override
|
||||
String get doctorPillRunning => 'running';
|
||||
|
||||
@override
|
||||
String get doctorPillStopped => 'stopped';
|
||||
|
||||
@override
|
||||
String get doctorDaemonControlHint =>
|
||||
'Studio shells out to the platform binary — mirrors `fai daemon …` exactly so the CLI and UI stay in lockstep.';
|
||||
|
||||
@override
|
||||
String get doctorDaemonControlWorking => 'Working…';
|
||||
|
||||
@override
|
||||
String get doctorPathLog => 'Log';
|
||||
|
||||
@override
|
||||
String get doctorPathConfig => 'Config';
|
||||
|
||||
@override
|
||||
String get doctorPathDb => 'Audit DB';
|
||||
|
||||
@override
|
||||
String get doctorPathModules => 'Modules dir';
|
||||
|
||||
@override
|
||||
String get doctorPathFlows => 'Flows dir';
|
||||
|
||||
@override
|
||||
String get doctorPathPid => 'PID file';
|
||||
|
||||
@override
|
||||
String get doctorPathsEmpty =>
|
||||
'Daemon did not report file paths. Update the running hub via `fai daemon restart`.';
|
||||
|
||||
@override
|
||||
String doctorOpenError(String detail) {
|
||||
return 'Could not open: $detail';
|
||||
}
|
||||
|
||||
@override
|
||||
String doctorUpdateAvailable(String version) {
|
||||
return 'Update available — $version';
|
||||
}
|
||||
|
||||
@override
|
||||
String doctorUpdateBody(String channel, String version) {
|
||||
return 'Channel $channel now offers $version. Apply via the button — Studio shells out to `fai update apply --channel $channel`.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorUpdateUnreachable => 'Release host unreachable';
|
||||
|
||||
@override
|
||||
String doctorUpdateUnreachableBody(String channel) {
|
||||
return 'Could not contact the release feed for $channel.';
|
||||
}
|
||||
|
||||
@override
|
||||
String doctorReleaseNotes(String url) {
|
||||
return 'notes: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get doctorPillNew => 'new';
|
||||
|
||||
@override
|
||||
String get doctorPillOffline => 'offline';
|
||||
|
||||
@override
|
||||
String get doctorApplyUpdate => 'Apply update';
|
||||
|
||||
@override
|
||||
String get doctorApplying => 'Applying…';
|
||||
|
||||
@override
|
||||
String get doctorApplyDone =>
|
||||
'Update applied. Restart Studio to see the new daemon version.';
|
||||
|
||||
@override
|
||||
String get mcpHeader => 'MCP CLIENTS';
|
||||
|
||||
|
|
|
|||
|
|
@ -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.24.0';
|
||||
const String kStudioVersion = '0.25.0';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class _DoctorPageState extends State<DoctorPage> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
tooltip: 'Re-check',
|
||||
tooltip: AppLocalizations.of(context)!.doctorRecheckTooltip,
|
||||
onPressed: _refresh,
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
|
|
@ -49,14 +49,15 @@ class _DoctorPageState extends State<DoctorPage> {
|
|||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return FaiEmptyState(
|
||||
icon: Icons.cloud_off_outlined,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
title: 'Hub unreachable',
|
||||
hint: 'Start the hub with `fai serve`.',
|
||||
title: l.hubUnreachable,
|
||||
hint: l.hubUnreachableHint,
|
||||
action: FilledButton.tonal(
|
||||
onPressed: _refresh,
|
||||
child: const Text('Retry'),
|
||||
child: Text(l.buttonRetry),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -72,7 +73,7 @@ class _DoctorPageState extends State<DoctorPage> {
|
|||
_SummaryStrip(snapshot: s),
|
||||
const SizedBox(height: FaiSpace.xl),
|
||||
_Section(
|
||||
title: 'Event log',
|
||||
title: AppLocalizations.of(context)!.doctorEventLogSection,
|
||||
child: _EventLogPanel(
|
||||
snapshot: s,
|
||||
onRefresh: _refresh,
|
||||
|
|
@ -80,22 +81,22 @@ class _DoctorPageState extends State<DoctorPage> {
|
|||
),
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_Section(
|
||||
title: 'Modules & approvals',
|
||||
title: AppLocalizations.of(context)!.doctorModulesApprovalsSection,
|
||||
child: _ModulesPanel(snapshot: s),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_Section(
|
||||
title: 'Host services',
|
||||
title: AppLocalizations.of(context)!.doctorHostServicesSection,
|
||||
child: _ServicesPanel(snapshot: s),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_Section(
|
||||
title: 'Daemon files',
|
||||
title: AppLocalizations.of(context)!.doctorDaemonFilesSection,
|
||||
child: _DaemonPathsPanel(paths: s.paths),
|
||||
),
|
||||
const SizedBox(height: FaiSpace.lg),
|
||||
_Section(
|
||||
title: 'Daemon control',
|
||||
title: AppLocalizations.of(context)!.doctorDaemonControlSection,
|
||||
child: _DaemonActionsCard(),
|
||||
),
|
||||
],
|
||||
|
|
@ -266,6 +267,7 @@ class _EventLogPanel extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final healthy = snapshot.chainHealthy;
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return FaiCard(
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
@ -280,9 +282,7 @@ class _EventLogPanel extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
healthy
|
||||
? 'Hash chain intact'
|
||||
: 'Tampering detected',
|
||||
healthy ? l.doctorChainIntact : l.doctorChainTampered,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
|
|
@ -290,8 +290,12 @@ class _EventLogPanel extends StatelessWidget {
|
|||
const SizedBox(height: 2),
|
||||
Text(
|
||||
healthy
|
||||
? '${snapshot.eventChainTotal} events · prev_event_sha256 verified end-to-end'
|
||||
: '${snapshot.eventChainVerified} of ${snapshot.eventChainTotal} verified before mismatch at ${snapshot.eventChainTamperedAt}',
|
||||
? l.doctorChainIntactDetail(snapshot.eventChainTotal)
|
||||
: l.doctorChainTamperedDetail(
|
||||
snapshot.eventChainVerified,
|
||||
snapshot.eventChainTotal,
|
||||
snapshot.eventChainTamperedAt ?? '',
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -302,11 +306,11 @@ class _EventLogPanel extends StatelessWidget {
|
|||
OutlinedButton.icon(
|
||||
onPressed: onRefresh,
|
||||
icon: const Icon(Icons.fact_check_outlined, size: 16),
|
||||
label: const Text('Verify now'),
|
||||
label: Text(l.doctorVerifyNow),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
FaiPill(
|
||||
label: healthy ? 'WORM-1' : 'TAMPER',
|
||||
label: healthy ? l.doctorChainPillOk : l.doctorChainPillTamper,
|
||||
tone: healthy ? FaiPillTone.success : FaiPillTone.danger,
|
||||
),
|
||||
],
|
||||
|
|
@ -326,20 +330,20 @@ class _DaemonPathsPanel extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final entries = <(String, String, IconData)>[
|
||||
('Log', paths.logPath, Icons.description_outlined),
|
||||
('Config', paths.configPath, Icons.settings_outlined),
|
||||
('Audit DB', paths.dbPath, Icons.storage_outlined),
|
||||
('Modules dir', paths.modulesDir, Icons.extension_outlined),
|
||||
('Flows dir', paths.flowsDir, Icons.account_tree_outlined),
|
||||
('PID file', paths.pidPath, Icons.fingerprint),
|
||||
(l.doctorPathLog, paths.logPath, Icons.description_outlined),
|
||||
(l.doctorPathConfig, paths.configPath, Icons.settings_outlined),
|
||||
(l.doctorPathDb, paths.dbPath, Icons.storage_outlined),
|
||||
(l.doctorPathModules, paths.modulesDir, Icons.extension_outlined),
|
||||
(l.doctorPathFlows, paths.flowsDir, Icons.account_tree_outlined),
|
||||
(l.doctorPathPid, paths.pidPath, Icons.fingerprint),
|
||||
].where((e) => e.$2.isNotEmpty).toList();
|
||||
|
||||
if (entries.isEmpty) {
|
||||
return FaiCard(
|
||||
child: Text(
|
||||
'Daemon did not report file paths. '
|
||||
'Update the running hub via `fai daemon restart`.',
|
||||
l.doctorPathsEmpty,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
);
|
||||
|
|
@ -396,13 +400,14 @@ class _PathRow extends StatelessWidget {
|
|||
final r = await SystemActions.openInOs(path);
|
||||
if (!context.mounted) return;
|
||||
if (!r.ok) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Could not open: ${r.stderr}')),
|
||||
SnackBar(content: Text(l.doctorOpenError(r.stderr))),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.open_in_new, size: 14),
|
||||
label: const Text('Open'),
|
||||
label: Text(AppLocalizations.of(context)!.buttonOpen),
|
||||
style: OutlinedButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
|
|
@ -468,6 +473,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
ChannelInfo? active;
|
||||
if (_channels != null) {
|
||||
for (final c in _channels!.channels) {
|
||||
|
|
@ -502,10 +508,10 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
|||
Expanded(
|
||||
child: Text(
|
||||
active == null
|
||||
? 'Daemon status: …'
|
||||
? l.doctorStatusUnknown
|
||||
: active.running
|
||||
? 'Running on ${active.name}: ${active.endpoint}'
|
||||
: '${active.name} daemon stopped',
|
||||
? l.doctorRunningOn(active.name, active.endpoint)
|
||||
: l.doctorStoppedOn(active.name),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
|
|
@ -514,22 +520,20 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
|||
),
|
||||
if (active != null) ...[
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Tooltip(
|
||||
message: 'PID file at the channel\'s run/<name>.pid',
|
||||
child: FaiPill(
|
||||
label: active.running ? 'running' : 'stopped',
|
||||
tone: active.running
|
||||
? FaiPillTone.success
|
||||
: FaiPillTone.neutral,
|
||||
),
|
||||
FaiPill(
|
||||
label: active.running
|
||||
? l.doctorPillRunning
|
||||
: l.doctorPillStopped,
|
||||
tone: active.running
|
||||
? FaiPillTone.success
|
||||
: FaiPillTone.neutral,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: FaiSpace.sm),
|
||||
Text(
|
||||
'Studio shells out to the platform binary — mirrors '
|
||||
'`fai daemon …` exactly so the CLI and UI stay in lockstep.',
|
||||
l.doctorDaemonControlHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -547,7 +551,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
|||
() => SystemActions.faiDaemon(['restart']),
|
||||
),
|
||||
icon: const Icon(Icons.restart_alt, size: 16),
|
||||
label: const Text('Restart'),
|
||||
label: Text(l.doctorRestart),
|
||||
),
|
||||
if (active?.running != true)
|
||||
FilledButton.icon(
|
||||
|
|
@ -558,7 +562,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
|||
() => SystemActions.faiDaemon(['start']),
|
||||
),
|
||||
icon: const Icon(Icons.play_arrow, size: 16),
|
||||
label: const Text('Start'),
|
||||
label: Text(l.doctorStart),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _busy
|
||||
|
|
@ -568,7 +572,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
|||
() => SystemActions.faiDaemon(['stop']),
|
||||
),
|
||||
icon: const Icon(Icons.stop_circle_outlined, size: 16),
|
||||
label: const Text('Stop'),
|
||||
label: Text(l.doctorStop),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _busy
|
||||
|
|
@ -578,7 +582,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
|||
() => SystemActions.faiDaemon(['status']),
|
||||
),
|
||||
icon: const Icon(Icons.health_and_safety_outlined, size: 16),
|
||||
label: const Text('Status'),
|
||||
label: Text(l.doctorStatusAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -593,7 +597,7 @@ class _DaemonActionsCardState extends State<_DaemonActionsCard> {
|
|||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Text(
|
||||
'Working…',
|
||||
l.doctorDaemonControlWorking,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -797,7 +801,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
|
|||
setState(() {
|
||||
_applying = false;
|
||||
_applyOutput = r.ok
|
||||
? 'Update applied. Restart Studio to see the new daemon version.'
|
||||
? AppLocalizations.of(context)!.doctorApplyDone
|
||||
: (r.stderr.isEmpty ? r.stdout : r.stderr).trim();
|
||||
});
|
||||
}
|
||||
|
|
@ -805,6 +809,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final status = widget.status;
|
||||
final available = status.updateAvailable;
|
||||
final iconData = available
|
||||
|
|
@ -814,13 +819,11 @@ class _UpdateBannerState extends State<_UpdateBanner> {
|
|||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant;
|
||||
final title = available
|
||||
? 'Update available — ${status.latestVersion}'
|
||||
: 'Release host unreachable';
|
||||
? l.doctorUpdateAvailable(status.latestVersion)
|
||||
: l.doctorUpdateUnreachable;
|
||||
final body = available
|
||||
? 'Channel ${status.channel} now offers ${status.latestVersion}. '
|
||||
'Apply via the button — Studio shells out to '
|
||||
'`fai update apply --channel ${status.channel}`.'
|
||||
: (status.reason ?? 'Could not contact the release feed for ${status.channel}.');
|
||||
? l.doctorUpdateBody(status.channel, status.latestVersion)
|
||||
: (status.reason ?? l.doctorUpdateUnreachableBody(status.channel));
|
||||
return FaiCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -851,7 +854,7 @@ class _UpdateBannerState extends State<_UpdateBanner> {
|
|||
if (available && status.releaseNotesUrl != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'notes: ${status.releaseNotesUrl}',
|
||||
l.doctorReleaseNotes(status.releaseNotesUrl!),
|
||||
style: FaiTheme.mono(
|
||||
size: 11,
|
||||
color: theme.colorScheme.primary,
|
||||
|
|
@ -871,11 +874,11 @@ class _UpdateBannerState extends State<_UpdateBanner> {
|
|||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.system_update_alt, size: 16),
|
||||
label: Text(_applying ? 'Applying…' : 'Apply update'),
|
||||
label: Text(_applying ? l.doctorApplying : l.doctorApplyUpdate),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
FaiPill(
|
||||
label: available ? 'new' : 'offline',
|
||||
label: available ? l.doctorPillNew : l.doctorPillOffline,
|
||||
tone: available ? FaiPillTone.success : FaiPillTone.neutral,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class _ModulesPageState extends State<ModulesPage> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
tooltip: 'Reload',
|
||||
tooltip: AppLocalizations.of(context)!.modulesReloadTooltip,
|
||||
onPressed: _refresh,
|
||||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
|
|
@ -54,6 +54,7 @@ class _ModulesPageState extends State<ModulesPage> {
|
|||
body: FutureBuilder<List<ModuleSummary>>(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
|
@ -61,22 +62,20 @@ class _ModulesPageState extends State<ModulesPage> {
|
|||
return FaiEmptyState(
|
||||
icon: Icons.cloud_off_outlined,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
title: 'Hub unreachable',
|
||||
hint:
|
||||
'Start the hub with `fai serve`. Studio will retry automatically.',
|
||||
title: l.hubUnreachable,
|
||||
hint: l.hubUnreachableHint,
|
||||
action: FilledButton.tonal(
|
||||
onPressed: _refresh,
|
||||
child: const Text('Retry'),
|
||||
child: Text(l.buttonRetry),
|
||||
),
|
||||
);
|
||||
}
|
||||
final modules = snapshot.data ?? [];
|
||||
if (modules.isEmpty) {
|
||||
return const FaiEmptyState(
|
||||
return FaiEmptyState(
|
||||
icon: Icons.extension_outlined,
|
||||
title: 'No modules yet',
|
||||
hint:
|
||||
'Run `fai install <capability-name>` or check ~/.fai/modules/.',
|
||||
title: l.modulesNoneTitle,
|
||||
hint: l.modulesNoneHint,
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
|
|
@ -142,7 +141,7 @@ class _ModuleCard extends StatelessWidget {
|
|||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
'Capabilities',
|
||||
AppLocalizations.of(context)!.modulesCapabilitiesLabel,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.4,
|
||||
|
|
@ -212,7 +211,7 @@ class _RecentActivityPanel extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'RECENT ACTIVITY',
|
||||
AppLocalizations.of(context)!.modulesRecentActivity,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
letterSpacing: 0.6,
|
||||
|
|
@ -221,7 +220,8 @@ class _RecentActivityPanel extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: FaiSpace.sm),
|
||||
Text(
|
||||
'last ${events.length} install/uninstall events from the audit log',
|
||||
AppLocalizations.of(context)!
|
||||
.modulesRecentActivityHint(events.length),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
|
@ -269,7 +269,7 @@ class _ActivityRow extends StatelessWidget {
|
|||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${installed ? "installed" : "uninstalled"} '
|
||||
'${installed ? AppLocalizations.of(context)!.modulesActivityInstalled : AppLocalizations.of(context)!.modulesActivityUninstalled} '
|
||||
'${event.moduleName ?? "(unknown)"}'
|
||||
'${event.moduleVersion != null ? " v${event.moduleVersion}" : ""}',
|
||||
style: theme.textTheme.bodySmall,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:flutter_markdown/flutter_markdown.dart';
|
|||
|
||||
import '../data/hub.dart';
|
||||
import '../data/system_actions.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
import '../theme/tokens.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
|
|
@ -220,9 +221,25 @@ class _StorePageState extends State<StorePage> {
|
|||
_status.isEmpty &&
|
||||
!_installedOnly &&
|
||||
items.any((e) => e.featured);
|
||||
// Sparse-store nudge: when no filter is set
|
||||
// and the store carries no federated entries,
|
||||
// hint that adding an MCP / n8n endpoint
|
||||
// brings dozens of capabilities at once.
|
||||
// Hides the moment any federated entry shows
|
||||
// up — the nudge has done its job.
|
||||
final showFederationNudge =
|
||||
_queryCtrl.text.trim().isEmpty &&
|
||||
_category.isEmpty &&
|
||||
_status.isEmpty &&
|
||||
!_installedOnly &&
|
||||
!items.any((e) => e.isFederated);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showFederationNudge) ...[
|
||||
const _FederationNudge(),
|
||||
const SizedBox(height: FaiSpace.md),
|
||||
],
|
||||
if (showFeatured) ...[
|
||||
_FeaturedStrip(
|
||||
items: items.where((e) => e.featured).toList(),
|
||||
|
|
@ -1773,3 +1790,66 @@ class _ScreenshotPlaceholder extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sparse-store nudge: the operator's store has only the
|
||||
/// bundled native + planned modules and no federated entries.
|
||||
/// The single biggest store-fullness lever is configuring an
|
||||
/// MCP server or n8n endpoint, so the banner says exactly
|
||||
/// that. Tap → Settings dialog opens straight to the editor.
|
||||
/// Dismisses automatically the next render after a federated
|
||||
/// entry shows up — no per-user "don't show again".
|
||||
class _FederationNudge extends StatelessWidget {
|
||||
const _FederationNudge();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final l = AppLocalizations.of(context)!;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(FaiSpace.md),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(FaiRadius.md),
|
||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.hub_outlined,
|
||||
size: 22,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l.storeFederationNudgeTitle,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l.storeFederationNudgeBody,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: FaiSpace.md),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () => FaiSettingsDialog.show(context),
|
||||
icon: const Icon(Icons.settings_outlined, size: 16),
|
||||
label: Text(l.storeFederationNudgeButton),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/hub.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../theme/theme.dart';
|
||||
import '../theme/tokens.dart';
|
||||
import 'fai_pill.dart';
|
||||
|
|
@ -48,19 +49,16 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
|||
}
|
||||
|
||||
Future<void> _confirmAndUninstall(ModuleDetail detail) async {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Uninstall module?'),
|
||||
content: Text(
|
||||
'Removes ${detail.name} v${detail.version} from this hub. '
|
||||
'Flows that reference it will fail at the next run. '
|
||||
'A `module.uninstalled` audit event is recorded.',
|
||||
),
|
||||
title: Text(l.modulesUninstallTitle),
|
||||
content: Text(l.modulesUninstallBody(detail.name, detail.version)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(l.buttonCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
|
|
@ -68,7 +66,7 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
|||
backgroundColor: Theme.of(ctx).colorScheme.error,
|
||||
foregroundColor: Theme.of(ctx).colorScheme.onError,
|
||||
),
|
||||
child: const Text('Uninstall'),
|
||||
child: Text(l.buttonUninstall),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -79,14 +77,16 @@ class _FaiModuleSheetState extends State<FaiModuleSheet> {
|
|||
final r = await HubService.instance.uninstallModule(detail.name);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Uninstalled ${r.name} v${r.version}.')),
|
||||
SnackBar(
|
||||
content: Text(l.modulesUninstalledToast(r.name, r.version)),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _uninstalling = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Uninstall failed: $e')),
|
||||
SnackBar(content: Text(l.modulesUninstallFailed(e.toString()))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -269,7 +269,11 @@ class _Body extends StatelessWidget {
|
|||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.delete_outline, size: 16),
|
||||
label: Text(uninstalling ? 'Uninstalling…' : 'Uninstall'),
|
||||
label: Text(
|
||||
uninstalling
|
||||
? AppLocalizations.of(context)!.modulesUninstalling
|
||||
: AppLocalizations.of(context)!.buttonUninstall,
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
side: BorderSide(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue