feat: guided setup — persona re-audit fixes (grade-1 round)

- 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 <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-13 01:20:59 +02:00
parent cf4024a4e2
commit ddac84ce8e
9 changed files with 308 additions and 33 deletions

View file

@ -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

View file

@ -72,6 +72,13 @@ Future<void> _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 `<outDir>/<name>.png`.
Future<void> _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'),
),
),
);

View file

@ -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 <name>` ein Rust+WASM-Scaffold erzeugen. Das module-sdk übernimmt die WIT-Plumbing; du schreibst nur ein typisiertes invoke.",
"welcomeChecklistNextModuleBody": "Mit `chain new module <name>` 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}).",

View file

@ -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}).",

View file

@ -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:

View file

@ -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 <name>` ein Rust+WASM-Scaffold erzeugen. Das module-sdk übernimmt die WIT-Plumbing; du schreibst nur ein typisiertes invoke.';
'Mit `chain new module <name>` 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';

View file

@ -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';

View file

@ -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<GuidedSetupDialog> {
@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<GuidedSetupDialog> {
'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<String> _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<GuidedSetupDialog> {
}
}
/// 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<void> _apply() async {
setState(() => _busy = true);
final path = await _writeAnswers();
@ -313,7 +332,15 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
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<GuidedSetupDialog> {
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<GuidedSetupDialog> {
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<GuidedSetupDialog> {
/// Build the localized plain-language plan lines from the structured
/// SetupPlan (never the CLI prose).
List<String> _planLines(AppLocalizations l, Map<String, dynamic> plan) {
final profile = plan['profile'] as String? ?? '';
final lines = <String>[];
switch (plan['profile'] as String? ?? '') {
switch (profile) {
case 'air-gapped':
lines.add(l.setupPlanIntroAirgapped);
case 'enterprise':
@ -832,13 +885,34 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
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<String>() ?? 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;
}

View file

@ -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);
},
);