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:
parent
cf4024a4e2
commit
ddac84ce8e
9 changed files with 308 additions and 33 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue