diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cd4326..0148005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,31 +5,6 @@ version + `kStudioVersion` in `lib/main.dart` stay in lockstep. ## Unreleased -### Fixed (usertest 2026-07-10 findings) - -- **Approval prompts localize.** An approval whose flow step gave no - `prompt:` (and legacy rows carrying the hub's old baked-in English - sentence) now renders the localized fallback "Freigabe für diesen - Schritt erforderlich" / "Approval required for this step" — no more - English inside the German approvals UI. -- **Approving without review data asks first.** When the approval step - attached no `show:` payload, "Freigeben" opens a calm confirmation - ("Ohne Prüfdaten freigeben?") explaining that the flow deliberately - attached no data, with an explicit "Trotzdem freigeben". -- **Studio error log moved to `~/.chain/logs/`.** Writes went to the - pre-rename `~/.fai/logs/studio-errors.log` while the Doctor page and - `chain doctor` read `~/.chain/…`. Studio now writes to `.chain` and - migrates the old file (+ rotation sibling) over once. -- **Honest audit wording (legal review).** DE strings no longer claim - "manipulationssicher" — the audit log is *manipulationserkennend* - (tamper-evident); "warum WORM-1 für KRITIS reicht" became a neutral - what-it-does-and-does-not sentence (EN too); the Doctor chain pill - says "Integritätskette v1" instead of "WORM-1"; the federation - enrollment hint says the CA *authenticates* the first connect. -- **WCAG-AA secondary text on dark.** De-emphasised text was 3.7:1 on - cards; the muted token is now ≥ 4.5:1 against canvas, cards and - elevated surfaces. - ### Added - **Live audit feed.** The Audit page subscribes to `streamEvents` and diff --git a/lib/data/chain_log.dart b/lib/data/chain_log.dart index d3411bf..440f23a 100644 --- a/lib/data/chain_log.dart +++ b/lib/data/chain_log.dart @@ -51,31 +51,7 @@ class ChainLog { Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '.'; - final path = p.join(home, '.chain', 'logs', 'studio-errors.log'); - _migrateLegacyLog(home, path); - return path; - } - - // Pre-rename installs wrote to `~/.fai/logs/`. Move that file (and - // its rotation sibling) over once so the error trail survives the - // rename; never overwrite an existing new-path file. Best-effort - // and cheap enough to run per access (two stat calls after the - // first migration). - static void _migrateLegacyLog(String home, String newPath) { - try { - for (final suffix in const ['', '.1']) { - final legacy = File( - p.join(home, '.fai', 'logs', 'studio-errors.log$suffix'), - ); - final target = File('$newPath$suffix'); - if (legacy.existsSync() && !target.existsSync()) { - target.parent.createSync(recursive: true); - legacy.renameSync(target.path); - } - } - } catch (_) { - // Best-effort: a failed migration must not break logging. - } + return p.join(home, '.fai', 'logs', 'studio-errors.log'); } /// Absolute path of the studio-errors log. Public so the diff --git a/lib/data/hub.dart b/lib/data/hub.dart index a10f431..df4b7fc 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -787,8 +787,6 @@ class HubService { provider: e.provider, sourceKind: e.sourceKind, source: e.source, - canonicalCategory: e.canonicalCategory, - canonicalCategoryLabel: e.canonicalCategoryLabel, ), ) .toList(); @@ -1802,20 +1800,6 @@ class StoreItem { /// label and group modules by where they came from. final String source; - /// Normalized category slug from the hub (e.g. "data", - /// "studio-themes"). The whole catalogue is mapped onto a fixed - /// taxonomy by capability namespace, so different stores' free-form - /// labels collapse into one bucket. Studio groups + filters by this, - /// not [category]. Empty from a pre-0.21 hub → fall back to [category]. - final String canonicalCategory; - - /// Human-readable label for [canonicalCategory] (e.g. "Data & Formats"). - final String canonicalCategoryLabel; - - /// True iff this entry is a Studio plugin/theme rather than a flow - /// module — drives the "Modules | Studio & Themes" store segment. - bool get isStudioPlugin => canonicalCategory == 'studio-themes'; - bool get isFederated => kind == 'federated'; const StoreItem({ @@ -1841,7 +1825,5 @@ class StoreItem { required this.provider, this.sourceKind = '', this.source = '', - this.canonicalCategory = '', - this.canonicalCategoryLabel = '', }); } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index d62b38f..470ec18 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -30,7 +30,7 @@ "welcomeTrustHeader": "TRUST-POSTURE", "welcomeTrustSandboxTitle": "Sandbox von Anfang an", "welcomeTrustSandboxBody": "Jedes Modul bringt eine explizite Berechtigungsliste mit — Netzwerk-Endpunkte, Dateien, Umgebungsvariablen. Der Hub setzt sie durch; ohne Operator-Freigabe verlässt nichts die Sandbox.", - "welcomeTrustAuditTitle": "Manipulationserkennendes Audit-Log", + "welcomeTrustAuditTitle": "Manipulationssicheres Audit-Log", "welcomeTrustAuditBody": "Flow-Läufe, Installationen, Deinstallationen, Freigabe-Entscheidungen — alles wird in ein hash-verkettetes Audit-Log geschrieben. Die Diagnose-Seite verifiziert die Kette bei jedem Laden komplett.", "welcomeTrustAirgapTitle": "Air-Gap-tauglich", "welcomeTrustAirgapBody": "Der gesamte Hub steckt in einem einzigen Binary für Linux, macOS und Windows. Sobald ein Modul installiert ist, läuft der Flow, der es nutzt, ohne weiteren Netzwerkzugriff — ideal für regulierte Umgebungen.", @@ -40,8 +40,8 @@ "welcomeDocArchitectureBlurb": "Hub, Modul, Flow — und wie die drei zusammenpassen.", "welcomeDocSecurityTitle": "Sandbox-Modell", "welcomeDocSecurityBlurb": "Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.", - "welcomeDocAuditTitle": "Manipulationserkennendes Audit-Log", - "welcomeDocAuditBlurb": "Wie die Hash-Kette nachträgliche Änderungen erkennbar macht — und was die Integritätsstufe WORM-1 leistet (und was nicht).", + "welcomeDocAuditTitle": "Manipulationssicheres Audit-Log", + "welcomeDocAuditBlurb": "Wie die Hash-Kette funktioniert und warum WORM-1 für KRITIS reicht.", "welcomeDocFlowsTitle": "Flow-Komposition", "welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.", "welcomeDocApprovalsTitle": "Freigaben", @@ -327,17 +327,6 @@ "storeCategoryChannel": "Kanäle", "storeCategoryWeb": "Web", "storeCategoryOrchestrator": "Orchestrierung", - "storeCatDocuments": "Dokumente", - "storeCatTextLanguage": "Text & Sprache", - "storeCatData": "Daten & Formate", - "storeCatAiLlm": "KI & LLM", - "storeCatWebApi": "Web & APIs", - "storeCatAnalysisDomain": "Analyse & Domäne", - "storeCatStudioThemes": "Studio & Themes", - "storeCatExamplesDev": "Beispiele & Dev", - "storeCatOther": "Sonstiges", - "storeSegmentModules": "Module", - "storeSegmentStudio": "Studio & Themes", "storeNResults": "{n} Treffer", "@storeNResults": { "placeholders": { @@ -1046,9 +1035,6 @@ "approvalsPillExpired": "abgelaufen", "approvalsPayloadPreview": "ZU PRÜFENDE DATEN", "approvalsNoPayload": "Keine Daten zum Prüfen angehängt. Der system.approval-Schritt des Flows bestimmt über sein \"show:\"-Feld, was angezeigt wird — setze es, um die Daten hinter dieser Entscheidung sichtbar zu machen.", - "approvalsNoDataConfirmTitle": "Ohne Prüfdaten freigeben?", - "approvalsNoDataConfirmBody": "Dieser Flow hat bewusst keine Prüfdaten hinterlegt (kein \"show:\" am Freigabe-Schritt). Du kannst trotzdem freigeben — entscheidest dann aber, ohne die Daten hinter dieser Entscheidung gesehen zu haben.", - "approvalsNoDataConfirmAction": "Trotzdem freigeben", "approvalsRequestFallback": "Freigabe für diesen Schritt erforderlich", "approvalsFlowStepMeta": "Flow: {flow} · Schritt: {step}", "approvalsApproveButton": "Freigeben", @@ -1196,7 +1182,7 @@ } } }, - "doctorChainPillOk": "Integritätskette v1", + "doctorChainPillOk": "WORM-1", "doctorChainPillTamper": "MANIPULIERT", "doctorVerifyNow": "Jetzt prüfen", "doctorRestart": "Neustart", @@ -1664,5 +1650,5 @@ "federationTokenLabel": "Bootstrap-Token (einmalig)", "federationConfigLabel": "Satelliten-Konfiguration (in den Satelliten einfügen)", "federationCopied": "In die Zwischenablage kopiert", - "federationEnrollmentHint": "Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA authentifiziert den ersten Connect des Satelliten." + "federationEnrollmentHint": "Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA macht den ersten Connect des Satelliten manipulationssicher." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e781046..a6572ee 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -49,7 +49,7 @@ "welcomeDocSecurityTitle": "Sandbox model", "welcomeDocSecurityBlurb": "What a module declares, what the operator caps, what the hub enforces.", "welcomeDocAuditTitle": "Tamper-evident audit log", - "welcomeDocAuditBlurb": "How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.", + "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", @@ -335,17 +335,6 @@ "storeCategoryChannel": "Channels", "storeCategoryWeb": "Web", "storeCategoryOrchestrator": "Orchestration", - "storeCatDocuments": "Documents", - "storeCatTextLanguage": "Text & Language", - "storeCatData": "Data & Formats", - "storeCatAiLlm": "AI & LLM", - "storeCatWebApi": "Web & APIs", - "storeCatAnalysisDomain": "Analysis & Domain", - "storeCatStudioThemes": "Studio & Themes", - "storeCatExamplesDev": "Examples & Dev", - "storeCatOther": "Other", - "storeSegmentModules": "Modules", - "storeSegmentStudio": "Studio & Themes", "storeNResults": "{n} result{n, plural, =1{} other{s}}", "@storeNResults": { "placeholders": { @@ -1064,9 +1053,6 @@ "approvalsPillExpired": "expired", "approvalsPayloadPreview": "DATA TO REVIEW", "approvalsNoPayload": "No data was attached for review. The flow's approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.", - "approvalsNoDataConfirmTitle": "Approve without review data?", - "approvalsNoDataConfirmBody": "This flow deliberately attached no review data (no \"show:\" on its approval step). You can still approve — but you would be deciding without seeing the data behind this decision.", - "approvalsNoDataConfirmAction": "Approve anyway", "approvalsRequestFallback": "Approval required for this step", "approvalsFlowStepMeta": "Flow: {flow} · Step: {step}", "@approvalsFlowStepMeta": { @@ -1220,7 +1206,7 @@ } } }, - "doctorChainPillOk": "Integrity chain v1", + "doctorChainPillOk": "WORM-1", "doctorChainPillTamper": "TAMPER", "doctorVerifyNow": "Verify now", "doctorRestart": "Restart", @@ -1703,5 +1689,5 @@ "federationTokenLabel": "Bootstrap token (single use)", "federationConfigLabel": "Satellite config (paste into the satellite)", "federationCopied": "Copied to clipboard", - "federationEnrollmentHint": "Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite's first connect." + "federationEnrollmentHint": "Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite's first connect tamper-proof." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 74e3d5c..bc599f3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -221,7 +221,7 @@ abstract class AppLocalizations { /// No description provided for @welcomeHeroSubtitle. /// /// In en, this message translates to: - /// **'Deterministic workflow engine for AI-assisted data processing in regulated environments.'** + /// **'Deterministic workflow engine for AI-assisted document processing in regulated environments.'** String get welcomeHeroSubtitle; /// No description provided for @welcomePillarHubTitle. @@ -347,7 +347,7 @@ abstract class AppLocalizations { /// No description provided for @welcomeDocAuditBlurb. /// /// In en, this message translates to: - /// **'How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.'** + /// **'How the hash chain works and why WORM-1 is enough for KRITIS.'** String get welcomeDocAuditBlurb; /// No description provided for @welcomeDocFlowsTitle. @@ -1526,72 +1526,6 @@ abstract class AppLocalizations { /// **'Orchestration'** String get storeCategoryOrchestrator; - /// No description provided for @storeCatDocuments. - /// - /// In en, this message translates to: - /// **'Documents'** - String get storeCatDocuments; - - /// No description provided for @storeCatTextLanguage. - /// - /// In en, this message translates to: - /// **'Text & Language'** - String get storeCatTextLanguage; - - /// No description provided for @storeCatData. - /// - /// In en, this message translates to: - /// **'Data & Formats'** - String get storeCatData; - - /// No description provided for @storeCatAiLlm. - /// - /// In en, this message translates to: - /// **'AI & LLM'** - String get storeCatAiLlm; - - /// No description provided for @storeCatWebApi. - /// - /// In en, this message translates to: - /// **'Web & APIs'** - String get storeCatWebApi; - - /// No description provided for @storeCatAnalysisDomain. - /// - /// In en, this message translates to: - /// **'Analysis & Domain'** - String get storeCatAnalysisDomain; - - /// No description provided for @storeCatStudioThemes. - /// - /// In en, this message translates to: - /// **'Studio & Themes'** - String get storeCatStudioThemes; - - /// No description provided for @storeCatExamplesDev. - /// - /// In en, this message translates to: - /// **'Examples & Dev'** - String get storeCatExamplesDev; - - /// No description provided for @storeCatOther. - /// - /// In en, this message translates to: - /// **'Other'** - String get storeCatOther; - - /// No description provided for @storeSegmentModules. - /// - /// In en, this message translates to: - /// **'Modules'** - String get storeSegmentModules; - - /// No description provided for @storeSegmentStudio. - /// - /// In en, this message translates to: - /// **'Studio & Themes'** - String get storeSegmentStudio; - /// No description provided for @storeNResults. /// /// In en, this message translates to: @@ -3254,24 +3188,6 @@ abstract class AppLocalizations { /// **'No data was attached for review. The flow\'s approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.'** String get approvalsNoPayload; - /// No description provided for @approvalsNoDataConfirmTitle. - /// - /// In en, this message translates to: - /// **'Approve without review data?'** - String get approvalsNoDataConfirmTitle; - - /// No description provided for @approvalsNoDataConfirmBody. - /// - /// In en, this message translates to: - /// **'This flow deliberately attached no review data (no \"show:\" on its approval step). You can still approve — but you would be deciding without seeing the data behind this decision.'** - String get approvalsNoDataConfirmBody; - - /// No description provided for @approvalsNoDataConfirmAction. - /// - /// In en, this message translates to: - /// **'Approve anyway'** - String get approvalsNoDataConfirmAction; - /// No description provided for @approvalsRequestFallback. /// /// In en, this message translates to: @@ -3545,7 +3461,7 @@ abstract class AppLocalizations { /// No description provided for @doctorChainPillOk. /// /// In en, this message translates to: - /// **'Integrity chain v1'** + /// **'WORM-1'** String get doctorChainPillOk; /// No description provided for @doctorChainPillTamper. @@ -4888,7 +4804,7 @@ abstract class AppLocalizations { /// No description provided for @federationEnrollmentHint. /// /// In en, this message translates to: - /// **'Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite\'s first connect.'** + /// **'Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite\'s first connect tamper-proof.'** String get federationEnrollmentHint; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index d7ddf32..5f8c754 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -110,7 +110,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Jedes Modul bringt eine explizite Berechtigungsliste mit — Netzwerk-Endpunkte, Dateien, Umgebungsvariablen. Der Hub setzt sie durch; ohne Operator-Freigabe verlässt nichts die Sandbox.'; @override - String get welcomeTrustAuditTitle => 'Manipulationserkennendes Audit-Log'; + String get welcomeTrustAuditTitle => 'Manipulationssicheres Audit-Log'; @override String get welcomeTrustAuditBody => @@ -145,11 +145,11 @@ class AppLocalizationsDe extends AppLocalizations { 'Was ein Modul deklariert, was der Operator deckelt, was der Hub durchsetzt.'; @override - String get welcomeDocAuditTitle => 'Manipulationserkennendes Audit-Log'; + String get welcomeDocAuditTitle => 'Manipulationssicheres Audit-Log'; @override String get welcomeDocAuditBlurb => - 'Wie die Hash-Kette nachträgliche Änderungen erkennbar macht — und was die Integritätsstufe WORM-1 leistet (und was nicht).'; + 'Wie die Hash-Kette funktioniert und warum WORM-1 für KRITIS reicht.'; @override String get welcomeDocFlowsTitle => 'Flow-Komposition'; @@ -820,39 +820,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get storeCategoryOrchestrator => 'Orchestrierung'; - @override - String get storeCatDocuments => 'Dokumente'; - - @override - String get storeCatTextLanguage => 'Text & Sprache'; - - @override - String get storeCatData => 'Daten & Formate'; - - @override - String get storeCatAiLlm => 'KI & LLM'; - - @override - String get storeCatWebApi => 'Web & APIs'; - - @override - String get storeCatAnalysisDomain => 'Analyse & Domäne'; - - @override - String get storeCatStudioThemes => 'Studio & Themes'; - - @override - String get storeCatExamplesDev => 'Beispiele & Dev'; - - @override - String get storeCatOther => 'Sonstiges'; - - @override - String get storeSegmentModules => 'Module'; - - @override - String get storeSegmentStudio => 'Studio & Themes'; - @override String storeNResults(int n) { return '$n Treffer'; @@ -1871,16 +1838,6 @@ class AppLocalizationsDe extends AppLocalizations { String get approvalsNoPayload => 'Keine Daten zum Prüfen angehängt. Der system.approval-Schritt des Flows bestimmt über sein \"show:\"-Feld, was angezeigt wird — setze es, um die Daten hinter dieser Entscheidung sichtbar zu machen.'; - @override - String get approvalsNoDataConfirmTitle => 'Ohne Prüfdaten freigeben?'; - - @override - String get approvalsNoDataConfirmBody => - 'Dieser Flow hat bewusst keine Prüfdaten hinterlegt (kein \"show:\" am Freigabe-Schritt). Du kannst trotzdem freigeben — entscheidest dann aber, ohne die Daten hinter dieser Entscheidung gesehen zu haben.'; - - @override - String get approvalsNoDataConfirmAction => 'Trotzdem freigeben'; - @override String get approvalsRequestFallback => 'Freigabe für diesen Schritt erforderlich'; @@ -2053,7 +2010,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get doctorChainPillOk => 'Integritätskette v1'; + String get doctorChainPillOk => 'WORM-1'; @override String get doctorChainPillTamper => 'MANIPULIERT'; @@ -2870,5 +2827,5 @@ class AppLocalizationsDe extends AppLocalizations { @override String get federationEnrollmentHint => - 'Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA authentifiziert den ersten Connect des Satelliten.'; + 'Übergib das Token dem Satelliten-Betreiber über einen sicheren Kanal. Die mitgelieferte CA macht den ersten Connect des Satelliten manipulationssicher.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 60802b4..cee3def 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -150,7 +150,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get welcomeDocAuditBlurb => - 'How the hash chain makes later edits detectable — and what integrity level WORM-1 does (and does not) provide.'; + 'How the hash chain works and why WORM-1 is enough for KRITIS.'; @override String get welcomeDocFlowsTitle => 'Flow composition'; @@ -833,39 +833,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get storeCategoryOrchestrator => 'Orchestration'; - @override - String get storeCatDocuments => 'Documents'; - - @override - String get storeCatTextLanguage => 'Text & Language'; - - @override - String get storeCatData => 'Data & Formats'; - - @override - String get storeCatAiLlm => 'AI & LLM'; - - @override - String get storeCatWebApi => 'Web & APIs'; - - @override - String get storeCatAnalysisDomain => 'Analysis & Domain'; - - @override - String get storeCatStudioThemes => 'Studio & Themes'; - - @override - String get storeCatExamplesDev => 'Examples & Dev'; - - @override - String get storeCatOther => 'Other'; - - @override - String get storeSegmentModules => 'Modules'; - - @override - String get storeSegmentStudio => 'Studio & Themes'; - @override String storeNResults(int n) { String _temp0 = intl.Intl.pluralLogic( @@ -1882,16 +1849,6 @@ class AppLocalizationsEn extends AppLocalizations { String get approvalsNoPayload => 'No data was attached for review. The flow\'s approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.'; - @override - String get approvalsNoDataConfirmTitle => 'Approve without review data?'; - - @override - String get approvalsNoDataConfirmBody => - 'This flow deliberately attached no review data (no \"show:\" on its approval step). You can still approve — but you would be deciding without seeing the data behind this decision.'; - - @override - String get approvalsNoDataConfirmAction => 'Approve anyway'; - @override String get approvalsRequestFallback => 'Approval required for this step'; @@ -2062,7 +2019,7 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get doctorChainPillOk => 'Integrity chain v1'; + String get doctorChainPillOk => 'WORM-1'; @override String get doctorChainPillTamper => 'TAMPER'; @@ -2873,5 +2830,5 @@ class AppLocalizationsEn extends AppLocalizations { @override String get federationEnrollmentHint => - 'Hand the token to the satellite operator over a secure channel. The bundled CA authenticates the satellite\'s first connect.'; + 'Hand the token to the satellite operator over a secure channel. The bundled CA makes the satellite\'s first connect tamper-proof.'; } diff --git a/lib/pages/approvals.dart b/lib/pages/approvals.dart index 9248df8..e13e168 100644 --- a/lib/pages/approvals.dart +++ b/lib/pages/approvals.dart @@ -10,24 +10,6 @@ import '../theme/tokens.dart'; import '../widgets/widgets.dart'; import 'welcome.dart' show showFaiDoc; -/// The fixed English sentence pre-0.21 hubs baked into stored -/// approvals when the flow gave no `prompt:`. Newer hubs store the -/// empty prompt verbatim. -const _legacyHubPromptDefault = - 'Please review and approve this step before continuing.'; - -/// Reviewer-facing prompt with fallbacks: an empty prompt (the flow -/// gave none) renders the localized default, and the legacy English -/// default from old hub rows is mapped onto the same localized -/// default so it stops showing English inside a German UI. -String displayApprovalPrompt(AppLocalizations l, String prompt) { - final trimmed = prompt.trim(); - if (trimmed.isEmpty || trimmed == _legacyHubPromptDefault) { - return l.approvalsRequestFallback; - } - return prompt; -} - class ApprovalsPage extends StatefulWidget { const ApprovalsPage({super.key}); @@ -86,29 +68,6 @@ class _ApprovalsPageState extends State Future _approve(ApprovalRecord a) async { final l = AppLocalizations.of(context)!; - // No `show:` data attached → never approve on a reflex. Calmly - // explain and ask for a conscious confirmation first. - if (a.payloadPreview == null) { - final confirmed = await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: Text(l.approvalsNoDataConfirmTitle), - content: Text(l.approvalsNoDataConfirmBody), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: Text(l.buttonCancel), - ), - FilledButton( - onPressed: () => Navigator.pop(ctx, true), - child: Text(l.approvalsNoDataConfirmAction), - ), - ], - ), - ); - if (confirmed != true) return; - } - if (!mounted) return; try { await HubService.instance.approve(a.id, _reviewer); _toast(l.approvalsApprovedToast(a.flowName, a.stepId)); @@ -118,7 +77,6 @@ class _ApprovalsPageState extends State } } - Future _reject(ApprovalRecord a) async { final l = AppLocalizations.of(context)!; final reason = await _promptReason(context); @@ -600,7 +558,9 @@ class _ApprovalCard extends StatelessWidget { // step left the prompt empty, so the card is never reduced to // a cryptic flow id. Text( - displayApprovalPrompt(l, approval.prompt), + approval.prompt.trim().isEmpty + ? l.approvalsRequestFallback + : approval.prompt, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), @@ -890,13 +850,7 @@ class _HistoryDialog extends StatelessWidget { ), ), const SizedBox(height: 4), - SelectableText( - displayApprovalPrompt( - AppLocalizations.of(context)!, - record.prompt, - ), - style: theme.textTheme.bodyMedium, - ), + SelectableText(record.prompt, style: theme.textTheme.bodyMedium), if (record.payloadPreview != null) ...[ const SizedBox(height: ChainSpace.md), Text( diff --git a/lib/pages/store.dart b/lib/pages/store.dart index 19b7e5c..d21a7bf 100644 --- a/lib/pages/store.dart +++ b/lib/pages/store.dart @@ -33,12 +33,6 @@ class _StorePageState extends State { String _category = ''; String _status = ''; - /// Store segment: `false` shows flow Modules, `true` shows Studio - /// plugins + themes. A theme extends the GUI, a module runs in a - /// flow — mixing them in one grid was a big part of the clutter, so - /// they live under a top segment toggle instead. - bool _showStudio = false; - /// Source filter: '' (all), 'native', 'mcp', 'n8n'. Applied /// client-side after the hub returns results — the search RPC /// has no source field. @@ -361,32 +355,6 @@ class _StorePageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Modules vs Studio plugins/themes — a theme - // extends the GUI, a module runs in a flow. - Padding( - padding: - const EdgeInsets.only(bottom: ChainSpace.md), - child: SegmentedButton( - segments: [ - ButtonSegment( - value: false, - label: Text(l.storeSegmentModules), - icon: const Icon(Icons.extension_outlined, - size: 16), - ), - ButtonSegment( - value: true, - label: Text(l.storeSegmentStudio), - icon: - const Icon(Icons.palette_outlined, size: 16), - ), - ], - selected: {_showStudio}, - showSelectedIcon: false, - onSelectionChanged: (s) => - setState(() => _showStudio = s.first), - ), - ), if (_aiThinking || _aiAnswer != null || _aiError != null) ...[ @@ -528,10 +496,6 @@ class _StorePageState extends State { /// card and the grid stay coherent. List _applyAllFilters(List items) { var out = _applySourceFilter(items); - // Store segment: flow modules vs Studio plugins/themes. Federated - // entries (MCP/n8n bridges) are never Studio plugins, so they stay - // in the Modules segment. - out = out.where((e) => e.isStudioPlugin == _showStudio).toList(); final ai = _aiMatchedNames; if (ai != null) { out = out.where((e) => ai.contains(e.name)).toList(); @@ -801,34 +765,6 @@ String _sourceForItem(StoreItem i) { /// data model — only the display side gets localised. Unknown /// ids fall through unchanged so newly-added categories don't /// vanish until we update this map. -/// Localized label for a canonical category slug (from the hub). Falls -/// back to the hub's English label / raw category for an unknown slug. -String _canonicalCatLabel(BuildContext ctx, String slug, String fallback) { - final l = AppLocalizations.of(ctx)!; - switch (slug) { - case 'documents': - return l.storeCatDocuments; - case 'text-language': - return l.storeCatTextLanguage; - case 'data': - return l.storeCatData; - case 'ai-llm': - return l.storeCatAiLlm; - case 'web-api': - return l.storeCatWebApi; - case 'analysis-domain': - return l.storeCatAnalysisDomain; - case 'studio-themes': - return l.storeCatStudioThemes; - case 'examples-dev': - return l.storeCatExamplesDev; - case 'other': - return l.storeCatOther; - default: - return fallback.isNotEmpty ? fallback : slug; - } -} - String _categoryDisplayName(BuildContext ctx, String wire) { final l = AppLocalizations.of(ctx)!; switch (wire) { @@ -1593,116 +1529,36 @@ class _StoreGrid extends StatelessWidget { required this.onInstall, }); - /// Fixed display order of the canonical categories (mirrors the - /// hub's `Category::all_ordered`) — flow-module categories first, - /// Studio + Other last, so the grouped store reads top-to-bottom - /// like an app store's category rows. - static const List _order = [ - 'documents', - 'text-language', - 'data', - 'ai-llm', - 'web-api', - 'analysis-domain', - 'examples-dev', - 'studio-themes', - 'other', - ]; - @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { const minCardWidth = 360.0; final cols = (constraints.maxWidth / minCardWidth).floor().clamp(1, 4); - - // Group the flat result set by canonical category so the - // store reads like an app store — a labelled section per - // category instead of one jumbled grid. Falls back to the - // raw `category` label for a pre-0.21 hub that sends no - // canonical slug. - final groups = >{}; - final labels = {}; - for (final it in items) { - final slug = it.canonicalCategory.isNotEmpty - ? it.canonicalCategory - : (it.category.isNotEmpty ? it.category : 'other'); - groups.putIfAbsent(slug, () => []).add(it); - labels[slug] = it.canonicalCategoryLabel.isNotEmpty - ? it.canonicalCategoryLabel - : (it.category.isNotEmpty ? it.category : 'Other'); - } - final slugs = groups.keys.toList() - ..sort((a, b) { - final ia = _order.indexOf(a); - final ib = _order.indexOf(b); - // Unknown slugs sort after the known order, alphabetically. - if (ia == -1 && ib == -1) return a.compareTo(b); - if (ia == -1) return 1; - if (ib == -1) return -1; - return ia.compareTo(ib); - }); - - Widget grid(List gi) => GridView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: cols, - mainAxisSpacing: ChainSpace.md, - crossAxisSpacing: ChainSpace.md, - mainAxisExtent: 168, - ), - itemCount: gi.length, - itemBuilder: (context, i) => _StoreCard( - item: gi[i], - locale: locale, - installedVersion: installedVersions[gi[i].name], - onTap: () => onTap(gi[i]), - onInstall: () => onInstall(gi[i]), - ), - ); - - // A single category (e.g. the store is already filtered to - // one) renders without a redundant header. - if (slugs.length <= 1) { - return grid(items); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (final slug in slugs) ...[ - Padding( - padding: const EdgeInsets.only( - top: ChainSpace.lg, - bottom: ChainSpace.sm, - ), - child: Row( - children: [ - Text( - _canonicalCatLabel(context, slug, labels[slug] ?? ''), - style: Theme.of(context) - .textTheme - .titleSmall - ?.copyWith(fontWeight: FontWeight.w700), - ), - const SizedBox(width: ChainSpace.sm), - Text( - '${groups[slug]!.length}', - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - fontFeatures: const [FontFeature.tabularFigures()], - ), - ), - ], - ), - ), - grid(groups[slug]!), - ], - ], + // shrinkWrap + NeverScrollable lets the grid sit inside + // the page-level SingleChildScrollView so editorial + // chrome and the grid scroll as one continuous surface + // (App-Store / Play-Store behaviour). Without this, the + // inner grid claims its own scroll viewport and the + // outer Column overflows on small windows. + return GridView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: cols, + mainAxisSpacing: ChainSpace.md, + crossAxisSpacing: ChainSpace.md, + mainAxisExtent: 168, + ), + itemCount: items.length, + itemBuilder: (context, i) => _StoreCard( + item: items[i], + locale: locale, + installedVersion: installedVersions[items[i].name], + onTap: () => onTap(items[i]), + onInstall: () => onInstall(items[i]), + ), ); }, ); @@ -3252,7 +3108,7 @@ const List _kFallbackTodayStories = [ badgeEn: 'AUDIT', badgeDe: 'AUDIT', titleEn: 'Tamper-evident hash chain — built in', - titleDe: 'Manipulation erkennbar per Hash-Kette — eingebaut', + titleDe: 'Manipulationssicher per Hash-Kette — eingebaut', bodyEn: 'Every flow run, every install, every approval lands in ~/.chain/audit/ as a hash-chained event log. Any later edit invalidates the chain. CRA-ready out of the box — no compliance product to buy on top.', bodyDe: diff --git a/lib/theme/tokens.dart b/lib/theme/tokens.dart index 135d714..fdd010d 100644 --- a/lib/theme/tokens.dart +++ b/lib/theme/tokens.dart @@ -27,9 +27,7 @@ class ChainColors { static const surface = Color(0xFF18181B); // cards static const surfaceHigh = Color(0xFF27272A); // elevated static const border = Color(0xFF3F3F46); // 1px outlines - // De-emphasised text. WCAG AA (≥4.5:1) against canvas, cards AND - // elevated surfaces — 0xFF71717A only reached 3.7:1 on cards. - static const muted = Color(0xFF8E8E97); + static const muted = Color(0xFF71717A); // de-emphasised text static const text = Color(0xFFE4E4E7); // body static const textStrong = Color(0xFFFAFAFA); // headings