From ddac84ce8e1f16539c2136ea343b0dc0ccbdae10 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Mon, 13 Jul 2026 01:20:59 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20guided=20setup=20=E2=80=94=20persona=20?= =?UTF-8?q?re-audit=20fixes=20(grade-1=20round)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Regulated path finishes without a terminal: the signed-source state offers 'Add a signed source…' (stores dialog with pin-a-key) plus the per-module install buttons and a plain-language hint why pinning the publisher's key matters — instead of a hint with no affordance. - Apply warnings (e.g. the empty-trusted-publishers caveat) surface selectable in the done state instead of being swallowed. - Truthful preview: new lines state which machine is being set up (server/container targets configure THIS machine), that regulated profiles always get the hash-chained audit log (even with WORM off), and that the curated reading list is stored with the setup record. - Language pass: onboarding checklist in Sie-form + 'System-KI' (was du-form + 'System-AI'), 'Audit-Sperre' jargon replaced, answers file moved to a private per-dialog temp dir. - Screenshot harness: GUIDE_SHOTS_THEME=light for light-parity proof runs. Signed-off-by: flemming-it --- CHANGELOG.md | 21 +++++++ integration_test/guide_shots_test.dart | 13 +++- lib/l10n/app_de.arb | 28 ++++++--- lib/l10n/app_en.arb | 12 +++- lib/l10n/app_localizations.dart | 56 ++++++++++++++++- lib/l10n/app_localizations_de.dart | 53 +++++++++++++--- lib/l10n/app_localizations_en.dart | 37 ++++++++++- lib/widgets/guided_setup_dialog.dart | 86 ++++++++++++++++++++++++-- test/guided_setup_test.dart | 35 ++++++++++- 9 files changed, 308 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d36537..bf5b2f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,27 @@ version + `kStudioVersion` in `lib/main.dart` stay in lockstep. silently skipped since the rename; it now resolves `$CHAIN_BIN`, `chain` on PATH, and `../fai_chain/target/{release,debug}/chain`. +### Added (guided setup — persona re-audit fixes, grade-1 round) + +- **Regulated path finishes without a terminal.** The post-apply + signed-source state now offers "Add a signed source…" (the stores + dialog with its pin-a-key field) plus the per-module install + buttons, instead of a hint with no affordance; a plain-language + trust hint explains why pinning the publisher's key matters. +- **Apply warnings surface.** Warning lines from `chain init --apply` + (e.g. the empty-trusted-publishers caveat) are shown selectable in + the done state instead of being swallowed on success. +- **Truthful preview.** New plan lines state which machine is being + set up (server/container targets configure THIS machine — said + explicitly), that regulated profiles get the hash-chained audit + log even when WORM is off, and that a curated reading list is + stored with the setup record. +- **Language pass.** Onboarding checklist switched to Sie-form and + "System-KI" (was du-form + "System-AI" next to the formal wizard); + "Audit-Sperre" jargon replaced with "Schreibschutz für das + Prüfprotokoll"; answers file now lives in a private per-dialog + temp dir instead of a fixed world-readable name. + ### Added (multi-project, stage ③ — sealed areas) - **Sealed-area connection switch.** The workspace switcher now lists diff --git a/integration_test/guide_shots_test.dart b/integration_test/guide_shots_test.dart index acb8f57..4d0ef7d 100644 --- a/integration_test/guide_shots_test.dart +++ b/integration_test/guide_shots_test.dart @@ -72,6 +72,13 @@ Future _pumpUntil( String get _outDir => Platform.environment['GUIDE_SHOTS_OUT'] ?? 'build/guide-shots'; +/// Guide images ship dark; GUIDE_SHOTS_THEME=light exists for the +/// light-parity proof runs (release gate: both themes must hold). +ThemeModeValue get _theme => + Platform.environment['GUIDE_SHOTS_THEME'] == 'light' + ? ThemeModeValue.light + : ThemeModeValue.dark; + /// Rasterize the app's RepaintBoundary into `/.png`. Future _shot(WidgetTester tester, String name) async { // Two extra frames so ripples/route transitions settle visually. @@ -179,9 +186,9 @@ void main() { await tester.pumpWidget( RepaintBoundary( key: _shotKey, - child: const StudioApp( - initialThemeMode: ThemeModeValue.dark, - initialLocale: Locale('de'), + child: StudioApp( + initialThemeMode: _theme, + initialLocale: const Locale('de'), ), ), ); diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 4933d00..5fff810 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -72,14 +72,14 @@ }, "welcomeChecklistHeader": "ERSTE SCHRITTE", "welcomeChecklistBody": "Vier Schritte zu einem funktionierenden Hub. Live-Zustand — nach jeder Änderung aktualisieren.", - "welcomeChecklistAi": "System-AI konfigurieren", - "welcomeChecklistAiHint": "Einstellungen → System-AI. Wird für die KI-Suche und Fehlererklärungen gebraucht.", + "welcomeChecklistAi": "System-KI konfigurieren", + "welcomeChecklistAiHint": "Einstellungen → System-KI. Wird für die KI-Suche und Fehlererklärungen gebraucht.", "welcomeChecklistMcp": "Öffentliche Capability-Quelle hinzufügen", "welcomeChecklistMcpHint": "Im Store eine MCP-Quelle hinzufügen oder unter Einstellungen → MCP-Clients konfigurieren.", "welcomeChecklistModule": "Ein Text-Modul installieren", - "welcomeChecklistModuleHint": "Im Store ein Modul aus der Kategorie deiner Wahl installieren. Module sind sandboxed Ch∆In-Komponenten.", + "welcomeChecklistModuleHint": "Im Store ein Modul aus der Kategorie Ihrer Wahl installieren. Module sind isolierte (sandboxed) Ch∆In-Komponenten.", "welcomeChecklistFlow": "Einen gespeicherten Flow starten", - "welcomeChecklistFlowHint": "Flows → einen wählen → Starten. Studio bringt einige Demo-Flows mit; auch ein eigener tut's.", + "welcomeChecklistFlowHint": "Flows → einen wählen → Starten. Studio bringt einige Demo-Flows mit; ein eigener funktioniert genauso.", "welcomeChecklistAllDone": "Alle vier Schritte erledigt.", "welcomeChecklistDismiss": "Checkliste ausblenden", "welcomeChecklistRefresh": "Aktualisieren", @@ -886,16 +886,16 @@ "maintenanceResetConfirmTitle": "Hub zurücksetzen?", "maintenanceResetConfirmBody": "Stoppt jeden Daemon, verschiebt ~/.chain/ in ein Backup mit Zeitstempel und startet sauber neu. Das Backup ist vollständig wiederherstellbar. Fortfahren?", "maintenanceResetConfirmButton": "Zurücksetzen", - "welcomeChecklistAllSetTitle": "Du bist eingerichtet.", - "welcomeChecklistAllSetBody": "Drei Stränge, an denen du als nächstes ziehen kannst:", + "welcomeChecklistAllSetTitle": "Sie sind eingerichtet.", + "welcomeChecklistAllSetBody": "Drei Stränge, an denen Sie als Nächstes ziehen können:", "welcomeChecklistNextAuditTitle": "Audit-Log lesen", - "welcomeChecklistNextAuditBody": "Jede Installation, jeder Flow-Lauf, jede Freigabe landet im hash-verketteten Log. Im Protokoll-Tab siehst du, was dein Hub gemacht hat.", + "welcomeChecklistNextAuditBody": "Jede Installation, jeder Flow-Lauf, jede Freigabe landet im hash-verketteten Protokoll. Im Protokoll-Tab sehen Sie, was Ihr Hub gemacht hat.", "welcomeChecklistNextAuditButton": "Protokoll öffnen", "welcomeChecklistNextTodayTitle": "Daily-Today-Story einrichten", "welcomeChecklistNextTodayBody": "tools/today/propose.sh per Cron laufen lassen und die Editorial-Karte im Store füllt sich mit operator-kuratierten Stories.", "welcomeChecklistNextTodayButton": "Today-Doku öffnen", "welcomeChecklistNextModuleTitle": "Eigenes Modul bauen", - "welcomeChecklistNextModuleBody": "Mit `chain new module ` ein Rust+WASM-Scaffold erzeugen. Das module-sdk übernimmt die WIT-Plumbing; du schreibst nur ein typisiertes invoke.", + "welcomeChecklistNextModuleBody": "Mit `chain new module ` ein Rust+WASM-Gerüst erzeugen. Das module-sdk übernimmt die WIT-Anbindung; Sie schreiben nur ein typisiertes invoke.", "welcomeChecklistNextModuleButton": "Doku lesen", "auditGroupToday": "HEUTE", "auditGroupYesterday": "GESTERN", @@ -1704,7 +1704,7 @@ "setupReviewTitle": "Das wird Ch∆In einrichten", "setupChooseFreeText": "Oder beschreiben Sie einfach, was Sie vorhaben", "setupScenTryingOut": "Erst mal ausprobieren", - "setupScenTryingOutSub": "Ein unkomplizierter Arbeitsplatz auf diesem Rechner — ohne Signaturen, ohne Audit-Sperre.", + "setupScenTryingOutSub": "Ein unkomplizierter Arbeitsplatz auf diesem Rechner — ohne Signaturpflicht, ohne Schreibschutz für das Prüfprotokoll.", "setupScenTeamHub": "Ein Team-Server", "setupScenTeamHubSub": "Ein abgesicherter Hub für mehrere Personen — signierte Module, Ressourcen-Grenzen.", "setupScenRegulated": "Regulierter Produktivbetrieb", @@ -1761,6 +1761,16 @@ "setupAllowUnsigned": "Installation aus dem öffentlichen Store erlauben", "setupAllowUnsignedSub": "Lockert die Signaturpflicht bewusst. Gut zum Ausprobieren — für den regulierten Betrieb später wieder aktivieren.", "setupStartCta": "In 3 Fragen loslegen", + "setupPlanRunbookLocal": "Eingerichtet wird dieser Rechner; der Hub läuft lokal und wird bei Bedarf gestartet.", + "setupPlanRunbookService": "Eingerichtet wird dieser Rechner: Der Hub startet künftig automatisch beim Hochfahren (Hintergrund-Dienst). Für einen anderen Server führen Sie die Einrichtung dort aus.", + "setupPlanRunbookAirgap": "Vorgesehen für einen isolierten Server ohne Internet — Module und Updates kommen als Offline-Paket.", + "setupPlanRunbookContainer": "Vorgesehen für den Betrieb im Container — ein fertiges Compose-Beispiel wird mitgeliefert.", + "setupPlanAuditChain": "Jeder Schritt landet in einem lückenlosen, hash-verketteten Prüfprotokoll — Manipulation wird erkennbar.", + "setupPlanDocs": "Eine passende Leseliste ({n} Kapitel) wird mit dem Einrichtungs-Protokoll unter ~/.chain/ gespeichert.", + "@setupPlanDocs": {"placeholders": {"n": {"type": "int"}}}, + "setupTrustedPublishersHint": "Wichtig: Hinterlegen Sie beim Hinzufügen der Quelle den Schlüssel des Herausgebers („Signatur-Schlüssel anheften“) — sonst akzeptiert der Hub jede formal gültige Signatur, egal von wem.", + "setupAddSignedSource": "Signierte Quelle hinzufügen…", + "setupApplyNotes": "Hinweise aus der Einrichtung", "setupFreeTextHint": "z. B.: Eingehende Anträge vorprüfen und unvollständige markieren", "setupFreeTextSuggest": "Vorschlagen lassen", "setupFreeTextPrivacyLocal": "Ihre Beschreibung wird lokal auf diesem Rechner verarbeitet ({model}).", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d8c90fa..3fd3e84 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1743,7 +1743,7 @@ "setupReviewTitle": "This is what Ch∆In will set up", "setupChooseFreeText": "Or just describe what you want to do", "setupScenTryingOut": "Just trying it out", - "setupScenTryingOutSub": "A relaxed workspace on this machine — no signing, no audit locking.", + "setupScenTryingOutSub": "A straightforward workstation on this machine — no signature requirement, no write-protected audit log.", "setupScenTeamHub": "A shared team hub", "setupScenTeamHubSub": "A secured hub for several people — signed modules, resource limits.", "setupScenRegulated": "Regulated production", @@ -1800,6 +1800,16 @@ "setupAllowUnsigned": "Allow installing from the public store", "setupAllowUnsignedSub": "Deliberately relaxes the signature requirement. Fine for trying things out — re-enable it for regulated operation.", "setupStartCta": "Get started in 3 questions", + "setupPlanRunbookLocal": "This machine is being set up; the hub runs locally and starts on demand.", + "setupPlanRunbookService": "This machine is being set up: the hub will start automatically on boot (background service). For a different server, run the setup there.", + "setupPlanRunbookAirgap": "Intended for an isolated server without internet — modules and updates arrive as offline bundles.", + "setupPlanRunbookContainer": "Intended for container operation — a ready-made compose example is included.", + "setupPlanAuditChain": "Every step lands in a gapless, hash-chained audit log — tampering becomes detectable.", + "setupPlanDocs": "A matching reading list ({n} chapters) is stored with the setup record under ~/.chain/.", + "@setupPlanDocs": {"placeholders": {"n": {"type": "int"}}}, + "setupTrustedPublishersHint": "Important: when adding the source, pin the publisher's key (\"pin signing key\") — otherwise the hub accepts any formally valid signature, no matter whose.", + "setupAddSignedSource": "Add a signed source…", + "setupApplyNotes": "Notes from the setup", "setupFreeTextHint": "e.g.: pre-screen incoming applications and flag incomplete ones", "setupFreeTextSuggest": "Suggest a setup", "setupFreeTextPrivacyLocal": "Your description is processed locally on this machine ({model}).", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7b44495..3a7f9ad 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5092,7 +5092,7 @@ abstract class AppLocalizations { /// No description provided for @setupScenTryingOutSub. /// /// In en, this message translates to: - /// **'A relaxed workspace on this machine — no signing, no audit locking.'** + /// **'A straightforward workstation on this machine — no signature requirement, no write-protected audit log.'** String get setupScenTryingOutSub; /// No description provided for @setupScenTeamHub. @@ -5395,6 +5395,60 @@ abstract class AppLocalizations { /// **'Get started in 3 questions'** String get setupStartCta; + /// No description provided for @setupPlanRunbookLocal. + /// + /// In en, this message translates to: + /// **'This machine is being set up; the hub runs locally and starts on demand.'** + String get setupPlanRunbookLocal; + + /// No description provided for @setupPlanRunbookService. + /// + /// In en, this message translates to: + /// **'This machine is being set up: the hub will start automatically on boot (background service). For a different server, run the setup there.'** + String get setupPlanRunbookService; + + /// No description provided for @setupPlanRunbookAirgap. + /// + /// In en, this message translates to: + /// **'Intended for an isolated server without internet — modules and updates arrive as offline bundles.'** + String get setupPlanRunbookAirgap; + + /// No description provided for @setupPlanRunbookContainer. + /// + /// In en, this message translates to: + /// **'Intended for container operation — a ready-made compose example is included.'** + String get setupPlanRunbookContainer; + + /// No description provided for @setupPlanAuditChain. + /// + /// In en, this message translates to: + /// **'Every step lands in a gapless, hash-chained audit log — tampering becomes detectable.'** + String get setupPlanAuditChain; + + /// No description provided for @setupPlanDocs. + /// + /// In en, this message translates to: + /// **'A matching reading list ({n} chapters) is stored with the setup record under ~/.chain/.'** + String setupPlanDocs(int n); + + /// No description provided for @setupTrustedPublishersHint. + /// + /// In en, this message translates to: + /// **'Important: when adding the source, pin the publisher\'s key (\"pin signing key\") — otherwise the hub accepts any formally valid signature, no matter whose.'** + String get setupTrustedPublishersHint; + + /// No description provided for @setupAddSignedSource. + /// + /// In en, this message translates to: + /// **'Add a signed source…'** + String get setupAddSignedSource; + + /// No description provided for @setupApplyNotes. + /// + /// In en, this message translates to: + /// **'Notes from the setup'** + String get setupApplyNotes; + /// No description provided for @setupFreeTextHint. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ef59997..87f56cf 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -235,11 +235,11 @@ class AppLocalizationsDe extends AppLocalizations { 'Vier Schritte zu einem funktionierenden Hub. Live-Zustand — nach jeder Änderung aktualisieren.'; @override - String get welcomeChecklistAi => 'System-AI konfigurieren'; + String get welcomeChecklistAi => 'System-KI konfigurieren'; @override String get welcomeChecklistAiHint => - 'Einstellungen → System-AI. Wird für die KI-Suche und Fehlererklärungen gebraucht.'; + 'Einstellungen → System-KI. Wird für die KI-Suche und Fehlererklärungen gebraucht.'; @override String get welcomeChecklistMcp => 'Öffentliche Capability-Quelle hinzufügen'; @@ -253,14 +253,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get welcomeChecklistModuleHint => - 'Im Store ein Modul aus der Kategorie deiner Wahl installieren. Module sind sandboxed Ch∆In-Komponenten.'; + 'Im Store ein Modul aus der Kategorie Ihrer Wahl installieren. Module sind isolierte (sandboxed) Ch∆In-Komponenten.'; @override String get welcomeChecklistFlow => 'Einen gespeicherten Flow starten'; @override String get welcomeChecklistFlowHint => - 'Flows → einen wählen → Starten. Studio bringt einige Demo-Flows mit; auch ein eigener tut\'s.'; + 'Flows → einen wählen → Starten. Studio bringt einige Demo-Flows mit; ein eigener funktioniert genauso.'; @override String get welcomeChecklistAllDone => 'Alle vier Schritte erledigt.'; @@ -1619,18 +1619,18 @@ class AppLocalizationsDe extends AppLocalizations { String get maintenanceResetConfirmButton => 'Zurücksetzen'; @override - String get welcomeChecklistAllSetTitle => 'Du bist eingerichtet.'; + String get welcomeChecklistAllSetTitle => 'Sie sind eingerichtet.'; @override String get welcomeChecklistAllSetBody => - 'Drei Stränge, an denen du als nächstes ziehen kannst:'; + 'Drei Stränge, an denen Sie als Nächstes ziehen können:'; @override String get welcomeChecklistNextAuditTitle => 'Audit-Log lesen'; @override String get welcomeChecklistNextAuditBody => - 'Jede Installation, jeder Flow-Lauf, jede Freigabe landet im hash-verketteten Log. Im Protokoll-Tab siehst du, was dein Hub gemacht hat.'; + 'Jede Installation, jeder Flow-Lauf, jede Freigabe landet im hash-verketteten Protokoll. Im Protokoll-Tab sehen Sie, was Ihr Hub gemacht hat.'; @override String get welcomeChecklistNextAuditButton => 'Protokoll öffnen'; @@ -1650,7 +1650,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get welcomeChecklistNextModuleBody => - 'Mit `chain new module ` ein Rust+WASM-Scaffold erzeugen. Das module-sdk übernimmt die WIT-Plumbing; du schreibst nur ein typisiertes invoke.'; + 'Mit `chain new module ` ein Rust+WASM-Gerüst erzeugen. Das module-sdk übernimmt die WIT-Anbindung; Sie schreiben nur ein typisiertes invoke.'; @override String get welcomeChecklistNextModuleButton => 'Doku lesen'; @@ -2991,7 +2991,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get setupScenTryingOutSub => - 'Ein unkomplizierter Arbeitsplatz auf diesem Rechner — ohne Signaturen, ohne Audit-Sperre.'; + 'Ein unkomplizierter Arbeitsplatz auf diesem Rechner — ohne Signaturpflicht, ohne Schreibschutz für das Prüfprotokoll.'; @override String get setupScenTeamHub => 'Ein Team-Server'; @@ -3182,6 +3182,41 @@ class AppLocalizationsDe extends AppLocalizations { @override String get setupStartCta => 'In 3 Fragen loslegen'; + @override + String get setupPlanRunbookLocal => + 'Eingerichtet wird dieser Rechner; der Hub läuft lokal und wird bei Bedarf gestartet.'; + + @override + String get setupPlanRunbookService => + 'Eingerichtet wird dieser Rechner: Der Hub startet künftig automatisch beim Hochfahren (Hintergrund-Dienst). Für einen anderen Server führen Sie die Einrichtung dort aus.'; + + @override + String get setupPlanRunbookAirgap => + 'Vorgesehen für einen isolierten Server ohne Internet — Module und Updates kommen als Offline-Paket.'; + + @override + String get setupPlanRunbookContainer => + 'Vorgesehen für den Betrieb im Container — ein fertiges Compose-Beispiel wird mitgeliefert.'; + + @override + String get setupPlanAuditChain => + 'Jeder Schritt landet in einem lückenlosen, hash-verketteten Prüfprotokoll — Manipulation wird erkennbar.'; + + @override + String setupPlanDocs(int n) { + return 'Eine passende Leseliste ($n Kapitel) wird mit dem Einrichtungs-Protokoll unter ~/.chain/ gespeichert.'; + } + + @override + String get setupTrustedPublishersHint => + 'Wichtig: Hinterlegen Sie beim Hinzufügen der Quelle den Schlüssel des Herausgebers („Signatur-Schlüssel anheften“) — sonst akzeptiert der Hub jede formal gültige Signatur, egal von wem.'; + + @override + String get setupAddSignedSource => 'Signierte Quelle hinzufügen…'; + + @override + String get setupApplyNotes => 'Hinweise aus der Einrichtung'; + @override String get setupFreeTextHint => 'z. B.: Eingehende Anträge vorprüfen und unvollständige markieren'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f114fa4..9313e75 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2993,7 +2993,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get setupScenTryingOutSub => - 'A relaxed workspace on this machine — no signing, no audit locking.'; + 'A straightforward workstation on this machine — no signature requirement, no write-protected audit log.'; @override String get setupScenTeamHub => 'A shared team hub'; @@ -3179,6 +3179,41 @@ class AppLocalizationsEn extends AppLocalizations { @override String get setupStartCta => 'Get started in 3 questions'; + @override + String get setupPlanRunbookLocal => + 'This machine is being set up; the hub runs locally and starts on demand.'; + + @override + String get setupPlanRunbookService => + 'This machine is being set up: the hub will start automatically on boot (background service). For a different server, run the setup there.'; + + @override + String get setupPlanRunbookAirgap => + 'Intended for an isolated server without internet — modules and updates arrive as offline bundles.'; + + @override + String get setupPlanRunbookContainer => + 'Intended for container operation — a ready-made compose example is included.'; + + @override + String get setupPlanAuditChain => + 'Every step lands in a gapless, hash-chained audit log — tampering becomes detectable.'; + + @override + String setupPlanDocs(int n) { + return 'A matching reading list ($n chapters) is stored with the setup record under ~/.chain/.'; + } + + @override + String get setupTrustedPublishersHint => + 'Important: when adding the source, pin the publisher\'s key (\"pin signing key\") — otherwise the hub accepts any formally valid signature, no matter whose.'; + + @override + String get setupAddSignedSource => 'Add a signed source…'; + + @override + String get setupApplyNotes => 'Notes from the setup'; + @override String get setupFreeTextHint => 'e.g.: pre-screen incoming applications and flag incomplete ones'; diff --git a/lib/widgets/guided_setup_dialog.dart b/lib/widgets/guided_setup_dialog.dart index 9ca9ed7..2ea36d8 100644 --- a/lib/widgets/guided_setup_dialog.dart +++ b/lib/widgets/guided_setup_dialog.dart @@ -21,6 +21,7 @@ import '../data/system_actions.dart'; import '../l10n/app_localizations.dart'; import '../main.dart' show StudioShellState; import '../theme/tokens.dart'; +import 'chain_stores_dialog.dart'; /// Allowed wire values per answer — an AI suggestion is validated /// against these; anything else is rejected as a parse failure so a @@ -238,6 +239,11 @@ class _GuidedSetupDialogState extends State { @override void dispose() { _goalCtl.dispose(); + try { + _answersDir?.deleteSync(recursive: true); + } on FileSystemException { + // Best-effort cleanup; the OS temp reaper covers the rest. + } super.dispose(); } @@ -265,8 +271,16 @@ class _GuidedSetupDialogState extends State { 'data_must_stay_local: $_dataLocal\n' 'allow_unsigned_modules: $_allowUnsigned\n'; + /// Per-dialog private temp dir for the answers file — a fixed + /// name in the shared system temp would be world-readable and + /// racy on multi-user machines. Cleaned up in [dispose]. + Directory? _answersDir; + Future _writeAnswers() async { - final f = File('${Directory.systemTemp.path}/chain-setup-answers.yaml'); + _answersDir ??= await Directory.systemTemp.createTemp('chain-setup-'); + final f = File( + '${_answersDir!.path}${Platform.pathSeparator}answers.yaml', + ); await f.writeAsString(_answersYaml()); return f.path; } @@ -304,6 +318,11 @@ class _GuidedSetupDialogState extends State { } } + /// Warning lines the apply emitted on success (e.g. the empty + /// trusted_publishers caveat). Swallowing them made the wizard + /// claim more than the config delivers — show them instead. + String _applyWarnings = ''; + Future _apply() async { setState(() => _busy = true); final path = await _writeAnswers(); @@ -313,7 +332,15 @@ class _GuidedSetupDialogState extends State { if (!mounted) return; setState(() => _busy = false); if (r.ok) { - setState(() => _applied = true); + final warnings = r.stderr + .split('\n') + .where((line) => line.toLowerCase().contains('warn')) + .join('\n') + .trim(); + setState(() { + _applied = true; + _applyWarnings = warnings; + }); unawaited(_probeHub()); } else { showFaiProcessError(context, 'chain init --apply', r.stdout, r.stderr); @@ -736,13 +763,24 @@ class _GuidedSetupDialogState extends State { label: Text(l.setupNextHubStart), ), ), - // 2 — the plan's modules. + // 2 — the plan's modules. On a signature-strict plan the path + // stays clickable: add a signed source (the stores dialog with + // its pin-a-key field), then install — no terminal, no dead end. if (modules.isNotEmpty) ...[ if (airGapped) _hintRow(l.setupModulesOfflineHint) - else if (sigGate) - _hintRow(l.setupModulesSignedHint) else ...[ + if (sigGate) ...[ + _hintRow(l.setupModulesSignedHint), + _hintRow(l.setupTrustedPublishersHint), + _actionRow( + OutlinedButton.icon( + onPressed: () => ChainStoresDialog.show(context), + icon: const Icon(Icons.add_moderator_outlined, size: 18), + label: Text(l.setupAddSignedSource), + ), + ), + ], if (!_hubUp) _hintRow(l.setupStartHubFirst), for (final m in modules) _installed.contains(m) @@ -776,6 +814,20 @@ class _GuidedSetupDialogState extends State { label: Text(l.setupActionOpenFlow(flow)), ), ), + // Honesty: warnings the apply emitted (e.g. the empty + // trusted-publishers caveat), selectable so they can go + // verbatim into a compliance note. + if (_applyWarnings.isNotEmpty) ...[ + const SizedBox(height: ChainSpace.sm), + Text(l.setupApplyNotes, style: theme.textTheme.labelLarge), + const SizedBox(height: 4), + SelectableText( + _applyWarnings, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], ]; } @@ -823,8 +875,9 @@ class _GuidedSetupDialogState extends State { /// Build the localized plain-language plan lines from the structured /// SetupPlan (never the CLI prose). List _planLines(AppLocalizations l, Map plan) { + final profile = plan['profile'] as String? ?? ''; final lines = []; - switch (plan['profile'] as String? ?? '') { + switch (profile) { case 'air-gapped': lines.add(l.setupPlanIntroAirgapped); case 'enterprise': @@ -832,13 +885,34 @@ class _GuidedSetupDialogState extends State { default: lines.add(l.setupPlanIntroDev); } + // Where this actually lands: the wizard always configures the + // machine Studio runs on — say so, especially when the operator + // picked a server/container target. + switch (plan['runbook'] as String? ?? '') { + case 'service': + lines.add(l.setupPlanRunbookService); + case 'air-gap-transfer': + lines.add(l.setupPlanRunbookAirgap); + case 'container': + lines.add(l.setupPlanRunbookContainer); + default: + lines.add(l.setupPlanRunbookLocal); + } if (plan['require_signatures'] == true) lines.add(l.setupPlanSignatures); + // The audit chain is always on for regulated profiles — the + // scenario card promises a tamper-evident log, so the preview + // states what is delivered (and the WORM line covers the rest). + if (profile == 'enterprise' || profile == 'air-gapped') { + lines.add(l.setupPlanAuditChain); + } if (plan['worm_audit'] == true) lines.add(l.setupPlanWorm); if (plan['approval_step'] == true) lines.add(l.setupPlanApproval); final modules = (plan['modules'] as List?)?.cast() ?? const []; if (modules.isNotEmpty) lines.add(l.setupPlanModules(modules.join(', '))); final flow = plan['starter_flow'] as String? ?? ''; if (flow.isNotEmpty) lines.add(l.setupPlanFlow(flow)); + final docs = (plan['curated_docs'] as List?)?.length ?? 0; + if (docs > 0) lines.add(l.setupPlanDocs(docs)); lines.add(l.setupPlanFileChanged('~/.chain/config.yaml')); return lines; } diff --git a/test/guided_setup_test.dart b/test/guided_setup_test.dart index 0faf348..7e3e6e6 100644 --- a/test/guided_setup_test.dart +++ b/test/guided_setup_test.dart @@ -163,17 +163,46 @@ void main() { ); testWidgets( - 'applied regulated (signed) state guides to the signed source instead of dead-end install buttons', + 'applied regulated (signed) state stays clickable — signed-source dialog, trust hint, install buttons', (tester) async { await tester.pumpWidget(_seededHost(_regulatedPlan(), applied: true)); await tester.tap(find.text('open')); await tester.pumpAndSettle(); + // The plain-language story + the pin-the-publisher trust hint. + expect(find.textContaining('signierten Quelle'), findsOneWidget); expect( - find.textContaining('signierten Quelle'), + find.textContaining('Schlüssel des Herausgebers'), + findsOneWidget, + ); + // No dead end: the stores dialog (with its pin-a-key field) is + // one click away, and the installs stay available for after + // the source is added. + expect(find.text('Signierte Quelle hinzufügen…'), findsOneWidget); + expect(find.text('text.extract installieren'), findsOneWidget); + expect(find.textContaining('chain store add'), findsNothing); + }, + ); + + testWidgets( + 'review preview names the machine being set up and the audit chain', + (tester) async { + await tester.pumpWidget(_seededHost(_regulatedPlan())); + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + + // Runbook honesty: a "home server" target still configures + // THIS machine — the preview must say so. + expect( + find.textContaining('Eingerichtet wird dieser Rechner'), + findsOneWidget, + ); + // The regulated promise is stated even when WORM is off: + // the hash-chained audit log line is always there. + expect( + find.textContaining('hash-verketteten Prüfprotokoll'), findsOneWidget, ); - expect(find.text('text.extract installieren'), findsNothing); }, );