diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5b2f5..5ca0b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,64 +5,6 @@ version + `kStudioVersion` in `lib/main.dart` stay in lockstep. ## Unreleased -### Added (guided setup on grade-1 — steps A4/A5/B1 + doc automation) - -- **Clickable next steps.** After apply, the wizard renders real - Studio actions instead of CLI text: start-hub button (polls until - the daemon answers), per-module install buttons with progress/done - states (capability-name install via the hub's store index), and an - open-the-starter-flow button navigating to Flows. -- **Signature dead end resolved.** Regulated plans explain in plain - language that modules come from a signed source; the preview offers - "allow installing from the public store" as one deliberate, - reversible switch (`allow_unsigned_modules`) that re-assembles the - plan. Air-gapped plans point to the offline-bundle path instead. -- **Fresh-install auto-open + honest framing.** On a fresh hub (no - config, no `setup-plan.yaml`) the wizard opens by itself, once per - run; the welcome CTA is now "In 3 Fragen loslegen" / "Get started - in 3 questions". After the wizard closes, the onboarding checklist - re-probes and states what the assistant already covered (applied - profile from `setup-plan.yaml`). -- **Free-text AI path (phase 1.2).** "Or just describe what you want - to do": the goal goes to the configured system AI, the reply is - validated against strict enum whitelists and comes back as an - editable "this is how I read your task" reflection feeding the same - preview/apply. Privacy line states local vs. provider processing; - without a system AI the menu path stands alone. -- **Nav manifest guard.** `test/nav_manifest_test.dart` generates - `docs/nav.generated.json` from the sidebar truth (ids, order = Cmd - numbers, DE+EN labels); the platform repo checks the operator guide - against the mirrored copy. -- **Guide screenshot harness.** `integration_test/guide_shots_test.dart` - boots a hermetic hub, seeds demo projects, walks every nav page, - the workspace switcher and the setup wizard, and writes the guide - PNGs — driven by the platform repo's `scripts/regen-studio-guide.sh`. -- **Fixed:** the integration-test hub fixture still looked for the - pre-rename `fai` binary and `chain_platform/` path, so its tests - 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/docs/nav.generated.json b/docs/nav.generated.json deleted file mode 100644 index 0a8ff7f..0000000 --- a/docs/nav.generated.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "_generated": "by test/nav_manifest_test.dart — do not edit. Mirror to fai_chain/docs/studio/nav.generated.json (same content).", - "pages": [ - { - "id": "welcome", - "cmd": 1, - "label_de": "Willkommen", - "label_en": "Welcome" - }, - { - "id": "store", - "cmd": 2, - "label_de": "Store", - "label_en": "Store" - }, - { - "id": "doctor", - "cmd": 3, - "label_de": "Diagnose", - "label_en": "Doctor" - }, - { - "id": "flows", - "cmd": 4, - "label_de": "Flows", - "label_en": "Flows" - }, - { - "id": "audit", - "cmd": 5, - "label_de": "Protokoll", - "label_en": "Audit" - }, - { - "id": "approvals", - "cmd": 6, - "label_de": "Freigaben", - "label_en": "Approvals" - }, - { - "id": "runs", - "cmd": 7, - "label_de": "Läufe", - "label_en": "Runs" - }, - { - "id": "federation", - "cmd": 8, - "label_de": "Föderation", - "label_en": "Federation" - } - ] -} diff --git a/integration_test/guide_shots_test.dart b/integration_test/guide_shots_test.dart deleted file mode 100644 index 4d0ef7d..0000000 --- a/integration_test/guide_shots_test.dart +++ /dev/null @@ -1,249 +0,0 @@ -// Deterministic operator-guide screenshot harness (one command -// instead of the fragile cliclick/screencapture procedure). -// -// Boots a hermetic hub (HubFixture: temp data dir, free port), -// seeds deterministic demo data via the CLI (an open "Bürgeramt" -// project; optionally a sealed "Ratsinformation" when the runner -// confirms an isolated $HOME), launches the real app in German + -// dark mode against that hub, walks every sidebar destination in -// nav order, opens the workspace switcher and the guided-setup -// wizard, and writes the guide PNGs via a RepaintBoundary capture -// (driverless — works headed on macOS with plain `flutter test`). -// -// Run through the platform repo's scripts/regen-studio-guide.sh, -// which isolates $HOME, builds the hub binary, and copies the -// PNGs into fai_chain/docs/studio/images/. Direct invocation: -// -// flutter test integration_test/guide_shots_test.dart -d macos -// -// Output lands in build/guide-shots/ unless GUIDE_SHOTS_OUT is -// set in the environment. -// -// Hermeticity notes: -// - SharedPreferences is mocked empty — the operator's real Studio -// prefs are neither read nor written. -// - The app reconnects to the fixture hub with persist: false. -// - The sealed seed writes to $HOME/.chain/sealed/ by design, so -// it only runs when CHAIN_GUIDE_SHOTS_SEALED=1 confirms the -// caller isolated $HOME (the regen script does). - -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart' show LogicalKeyboardKey; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:chain_client_sdk/chain_client_sdk.dart' show HubEndpoint; -import 'package:chain_studio/data/hub.dart'; -import 'package:chain_studio/main.dart'; -import 'package:chain_studio/widgets/widgets.dart'; - -import '../test/integration/hub_fixture.dart'; - -final GlobalKey _shotKey = GlobalKey(); - -/// Pump a fixed number of frames instead of pumpAndSettle: the -/// sidebar clock ticks every second, so the tree never settles and -/// pumpAndSettle degenerates into its multi-minute timeout. -Future _pumpFrames(WidgetTester tester, [int frames = 20]) async { - for (var i = 0; i < frames; i++) { - await tester.pump(const Duration(milliseconds: 100)); - } -} - -/// Pump until [finder] matches (page content arrived) or ~10 s -/// pass — pages load their data from the hub asynchronously, and a -/// blind fixed wait raced the slower ones (the flows editor). -Future _pumpUntil( - WidgetTester tester, - Finder finder, { - int maxFrames = 100, -}) async { - for (var i = 0; i < maxFrames; i++) { - if (finder.evaluate().isNotEmpty) return; - await tester.pump(const Duration(milliseconds: 100)); - } -} - -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. - await tester.pump(const Duration(milliseconds: 120)); - await tester.pump(const Duration(milliseconds: 120)); - final boundary = - _shotKey.currentContext!.findRenderObject() as RenderRepaintBoundary; - final image = await boundary.toImage(pixelRatio: 2.0); - final bytes = await image.toByteData(format: ui.ImageByteFormat.png); - image.dispose(); - final file = File('$_outDir/$name.png'); - file.parent.createSync(recursive: true); - file.writeAsBytesSync(bytes!.buffer.asUint8List()); - // ignore: avoid_print - print('guide-shot: ${file.path}'); -} - -Future _cli( - String binary, - HubFixture fixture, - List args, -) async { - final r = await Process.run( - binary, - args, - environment: { - ...Platform.environment, - 'CHAIN_DATA_DIR': fixture.tempDir.path, - 'CHAIN_MODULES_DIR': '${fixture.tempDir.path}/modules', - }, - ); - if (r.exitCode != 0) { - fail('seed command `chain ${args.join(' ')}` failed: ${r.stderr}'); - } -} - -/// The guide's image set, in sidebar (= Cmd number) order. Must -/// stay aligned with docs/nav.generated.json — the nav guard in -/// the platform repo keeps the guide itself honest. -const _pageShots = [ - ('welcome', '01-willkommen'), - ('store', '02-store'), - ('doctor', '03-diagnose'), - ('flows', '04-flows'), - ('audit', '05-protokoll'), - ('approvals', '06-freigaben'), - ('runs', '07-laeufe'), - ('federation', '08-foederation'), -]; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('capture the operator-guide screenshots', (tester) async { - final fixture = await HubFixture.start(skipIfBinaryMissing: false); - addTearDown(fixture!.dispose); - final binary = (await HubFixture.binaryPath())!; - - // Deterministic demo data. The slug is explicit — the derived - // one would drop the umlaut ("brgeramt"). - await _cli(binary, fixture, [ - 'project', - 'create', - 'Bürgeramt', - '--slug', - 'buergeramt', - '--color', - '#2e8f9e', - ]); - // The sealed area writes a manifest under $HOME/.chain/sealed/, - // so it needs the runner's confirmation that $HOME is isolated. - final sealedSeed = - Platform.environment['CHAIN_GUIDE_SHOTS_SEALED'] == '1'; - if (sealedSeed) { - await _cli(binary, fixture, [ - 'project', - 'create', - 'Ratsinformation', - '--slug', - 'ratsinformation', - '--isolation', - 'sealed', - ]); - // Seed one audit event the guide can show (setup.applied) by - // applying a minimal guided setup against the fixture's data - // dir. Writes $HOME/.chain/config.yaml, hence the isolated- - // $HOME guard: only the regen script's throwaway HOME is - // ever touched. - final answers = File('${fixture.tempDir.path}/seed-answers.yaml'); - answers.writeAsStringSync( - 'scenario: trying-out\nintent: hello-world\ntarget: this-laptop\n', - ); - await _cli(binary, fixture, [ - 'init', - '--answers', - answers.path, - '--apply', - '--force', - ]); - } - - // Hermetic prefs: never read or write the operator's real ones. - SharedPreferences.setMockInitialValues({}); - - await tester.pumpWidget( - RepaintBoundary( - key: _shotKey, - child: StudioApp( - initialThemeMode: _theme, - initialLocale: const Locale('de'), - ), - ), - ); - await HubService.instance.reconnect( - HubEndpoint(host: '127.0.0.1', port: fixture.port), - authToken: null, - persist: false, - ); - await _pumpFrames(tester, 30); - - final shell = - tester.state(find.byType(StudioShell)); - - // 01–08: every sidebar destination, in Cmd order. Where a page - // loads slower content from the hub, wait for a sentinel that - // proves the content arrived before capturing. - for (final (id, shotName) in _pageShots) { - shell.navigateTo(id); - await _pumpFrames(tester); - if (id == 'flows') { - // The imported sample flows populate the file list. - await _pumpUntil(tester, find.textContaining('extract-summarize')); - } else if (id == 'audit') { - // The seeded setup.applied event (isolated-$HOME runs). - await _pumpUntil(tester, find.textContaining('setup.applied')); - } - await _shot(tester, shotName); - } - - // 09: the workspace switcher, opened (shows the demo projects; - // with the sealed seed also the shield entry). - shell.navigateTo('audit'); - await _pumpFrames(tester); - final switcher = find.byType(ChainWorkspaceSwitcher); - if (switcher.evaluate().isNotEmpty) { - await tester.tap(switcher.first); - await _pumpFrames(tester); - await _shot(tester, '09-projekt-umschalter'); - // Close the menu again before moving on. - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await _pumpFrames(tester, 5); - } - - // 11: the guided-setup wizard, step 1 (localized option cards). - // 10 (inside a sealed area) still needs the manual procedure — - // it requires switching to a second, isolated hub instance. - shell.navigateTo('welcome'); - await _pumpFrames(tester); - final cta = find.text('In 3 Fragen loslegen'); - if (cta.evaluate().isNotEmpty) { - await tester.tap(cta.first); - await _pumpFrames(tester); - await _shot(tester, '11-einrichtung'); - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await _pumpFrames(tester, 5); - } - }); -} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c29d29b..cefdd8d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -4,7 +4,7 @@ "errModuleDownloadHint": "Die Bundle-URL des Moduls ist nicht erreichbar (Statuscode unten). Der Hub läuft — URL/Host korrigieren oder anderen Store wählen.", "storeFromSource": "aus {store}", "guidedSetupTitle": "Setup-Assistent", - "guidedSetupIntro": "Beantworten Sie drei Fragen — Ch∆In stellt passende Konfiguration, Modul-Set und Beispiel-Flow zusammen.", + "guidedSetupIntro": "Beantworte drei Fragen — Ch∆In stellt passende Konfiguration, Modul-Set und Start-Flow zusammen.", "guidedSetupScenario": "Szenario", "guidedSetupIntent": "Aufgabe", "guidedSetupTarget": "Wo läuft es?", @@ -72,14 +72,14 @@ }, "welcomeChecklistHeader": "ERSTE SCHRITTE", "welcomeChecklistBody": "Vier Schritte zu einem funktionierenden Hub. Live-Zustand — nach jeder Änderung aktualisieren.", - "welcomeChecklistAi": "System-KI konfigurieren", - "welcomeChecklistAiHint": "Einstellungen → System-KI. Wird für die KI-Suche und Fehlererklärungen gebraucht.", + "welcomeChecklistAi": "System-AI konfigurieren", + "welcomeChecklistAiHint": "Einstellungen → System-AI. Wird für die KI-Suche und Fehlererklärungen gebraucht.", "welcomeChecklistMcp": "Öffentliche Capability-Quelle hinzufügen", - "welcomeChecklistMcpHint": "Verbindet einen öffentlichen Werkzeug-Katalog (MCP) — dessen Bausteine erscheinen dann im Store. Dort hinzufügen oder unter Einstellungen → MCP-Clients.", + "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 Ihrer Wahl installieren. Module sind isolierte (sandboxed) Ch∆In-Komponenten.", + "welcomeChecklistModuleHint": "Im Store ein Modul aus der Kategorie deiner Wahl installieren. Module sind sandboxed Ch∆In-Komponenten.", "welcomeChecklistFlow": "Einen gespeicherten Flow starten", - "welcomeChecklistFlowHint": "Flows → einen wählen → Starten. Studio bringt einige Demo-Flows mit; ein eigener funktioniert genauso.", + "welcomeChecklistFlowHint": "Flows → einen wählen → Starten. Studio bringt einige Demo-Flows mit; auch ein eigener tut's.", "welcomeChecklistAllDone": "Alle vier Schritte erledigt.", "welcomeChecklistDismiss": "Checkliste ausblenden", "welcomeChecklistRefresh": "Aktualisieren", @@ -349,7 +349,7 @@ "storeReloadTooltip": "Aktualisieren", "storesManagerButton": "Store hinzufügen", "storesManagerTitle": "Module-Stores", - "storesManagerIntro": "Der Hub führt die Module aller Stores unten zu einem Index zusammen. Fügen Sie die Index-URL eines Stores hinzu, damit seine Module hier installierbar werden.", + "storesManagerIntro": "Der Hub führt die Module aller Stores unten zu einem Index zusammen. Füge die Index-URL eines Stores hinzu, damit seine Module hier installierbar werden.", "storesManagerAddSection": "Store hinzufügen", "storesManagerNameLabel": "Name", "storesManagerUrlLabel": "Index-URL (https:// oder file://)", @@ -360,8 +360,8 @@ "storesManagerRemoveFailed": "Store konnte nicht entfernt werden", "storesManagerSuggested": "Vorgeschlagene Stores", "storesManagerAddShort": "Hinzufügen", - "storesManagerPinnedKeyLabel": "Signatur-Schlüssel anheften (PEM, optional)", - "storesManagerPinnedKeyHint": "Nur Module, die mit diesem Schlüssel signiert sind, werden aus dieser Quelle angenommen. Fügen Sie den öffentlichen Schlüssel (PEM) des Herausgebers ein — statt des eingebauten Herstellerschlüssels.", + "storesManagerPinnedKeyLabel": "Angepinnter Public-Key — PEM (optional)", + "storesManagerPinnedKeyHint": "Signierte Module dieses Stores werden gegen diesen Key geprüft statt gegen den eingebauten Vendor-Key. Füge einen PEM-Public-Key ein.", "storesManagerNotAvailable": "Noch nicht verfügbar", "storesManagerNotAvailableHint": "Der Anbieter hat diesen Store-Index noch nicht veröffentlicht. Sobald er das tut, ist er installierbar.", "storesSuggestedReclaimDesc": "Recl∆Im — Module für Rechts- und Pflichten-Reform-Mapping", @@ -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": "Sie sind eingerichtet.", - "welcomeChecklistAllSetBody": "Drei Stränge, an denen Sie als Nächstes ziehen können:", + "welcomeChecklistAllSetTitle": "Du bist eingerichtet.", + "welcomeChecklistAllSetBody": "Drei Stränge, an denen du als nächstes ziehen kannst:", "welcomeChecklistNextAuditTitle": "Audit-Log lesen", - "welcomeChecklistNextAuditBody": "Jede Installation, jeder Flow-Lauf, jede Freigabe landet im hash-verketteten Protokoll. Im Protokoll-Tab sehen Sie, was Ihr Hub gemacht hat.", + "welcomeChecklistNextAuditBody": "Jede Installation, jeder Flow-Lauf, jede Freigabe landet im hash-verketteten Log. Im Protokoll-Tab siehst du, was dein Hub gemacht hat.", "welcomeChecklistNextAuditButton": "Protokoll öffnen", "welcomeChecklistNextTodayTitle": "Daily-Today-Story einrichten", - "welcomeChecklistNextTodayBody": "Für Fortgeschrittene: das Skript tools/today/propose.sh regelmäßig (Cron) laufen lassen — die Editorial-Karte im Store füllt sich dann mit kuratierten Stories.", + "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": "Für Entwickler: Mit `chain new module ` entsteht ein fertiges Modul-Gerüst (Rust+WASM). Das module-sdk übernimmt die technische Anbindung; Sie schreiben nur die eigentliche Logik.", + "welcomeChecklistNextModuleBody": "Mit `chain new module ` ein Rust+WASM-Scaffold erzeugen. Das module-sdk übernimmt die WIT-Plumbing; du schreibst 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 Signaturpflicht, ohne Schreibschutz für das Prüfprotokoll.", + "setupScenTryingOutSub": "Ein unkomplizierter Arbeitsplatz auf diesem Rechner — ohne Signaturen, ohne Audit-Sperre.", "setupScenTeamHub": "Ein Team-Server", "setupScenTeamHubSub": "Ein abgesicherter Hub für mehrere Personen — signierte Module, Ressourcen-Grenzen.", "setupScenRegulated": "Regulierter Produktivbetrieb", @@ -1747,53 +1747,7 @@ "setupApplied": "Fertig — Ch∆In ist eingerichtet.", "setupNextTitle": "Nächste Schritte", "setupNextHubStart": "Hub starten", - "setupHubRunning": "Der Hub läuft.", - "setupStartHubFirst": "Starten Sie zuerst den Hub — dann lassen sich die Module hier installieren.", - "setupActionInstall": "{module} installieren", - "@setupActionInstall": {"placeholders": {"module": {"type": "String"}}}, - "setupActionInstalled": "{module} ist installiert.", - "@setupActionInstalled": {"placeholders": {"module": {"type": "String"}}}, - "setupActionOpenFlow": "Beispiel-Flow „{flow}“ öffnen", - "@setupActionOpenFlow": {"placeholders": {"flow": {"type": "String"}}}, - "setupModulesOfflineHint": "Die Module kommen ohne Internet als Offline-Paket auf den Server — die Anleitung dazu steht in der Dokumentation unter „Betrieb ohne Internet“.", - "setupModulesSignedHint": "Die Module kommen aus einer eigenen, signierten Quelle — nicht aus dem öffentlichen Store.", - "setupSigPublicStoreNotice": "Diese Einrichtung lässt nur signierte Module zu. Der öffentliche Store liefert zurzeit unsignierte Pakete — die vorbereiteten Module kommen daher aus einer eigenen, signierten Quelle.", - "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}).", - "@setupFreeTextPrivacyLocal": {"placeholders": {"model": {"type": "String"}}}, - "setupFreeTextPrivacyRemote": "Ihre Beschreibung wird an die eingerichtete System-KI gesendet: {model} ({provider}).", - "@setupFreeTextPrivacyRemote": {"placeholders": {"model": {"type": "String"}, "provider": {"type": "String"}}}, - "setupFreeTextUnavailable": "Für den Freitext-Weg braucht Ch∆In eine eingerichtete System-KI (Einstellungen → System-KI). Die Auswahl oben funktioniert immer — ganz ohne KI.", - "setupFreeTextParseError": "Die System-KI hat keinen verwertbaren Vorschlag geliefert. Wählen Sie oben aus dem Menü — oder versuchen Sie es noch einmal.", - "setupReflectionTitle": "So verstehe ich Ihre Aufgabe", - "setupReflectionScenario": "Worum es geht: {label}", - "@setupReflectionScenario": {"placeholders": {"label": {"type": "String"}}}, - "setupReflectionIntent": "Erste Aufgabe: {label}", - "@setupReflectionIntent": {"placeholders": {"label": {"type": "String"}}}, - "setupReflectionTarget": "Umgebung: {label}", - "@setupReflectionTarget": {"placeholders": {"label": {"type": "String"}}}, - "setupReflectionApproval": "Vor jedem KI-Schritt wird ein Mensch um Freigabe gebeten.", - "setupReflectionDataLocal": "Daten bleiben vollständig im Haus.", - "setupReflectionEditHint": "Es wird nichts ohne Ihre Bestätigung geändert. Passen Sie die Punkte an oder gehen Sie direkt zur Vorschau.", - "setupReflectionAdjust": "Anpassen", - "setupReflectionToPreview": "Zur Vorschau", - "setupProfileDev": "unkomplizierter Arbeitsplatz", - "setupProfileEnterprise": "abgesicherter Team-Betrieb", - "setupProfileAirgapped": "vollständig lokaler, regulierter Betrieb", - "welcomeChecklistSetupDone": "Die Grundeinrichtung ({profile}) hat der Einrichtungs-Assistent bereits erledigt — hier sehen Sie, was auf dem Hub schon passiert ist.", - "@welcomeChecklistSetupDone": {"placeholders": {"profile": {"type": "String"}}} + "setupNextInstall": "Die Module installieren", + "setupNextRunFlow": "Den Beispiel-Flow ausführen", + "setupStartCta": "In 3 Fragen loslegen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e56e517..cfb73e2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -378,8 +378,8 @@ "storesManagerRemoveFailed": "Could not remove store", "storesManagerSuggested": "Suggested stores", "storesManagerAddShort": "Add", - "storesManagerPinnedKeyLabel": "Pin a signing key (PEM, optional)", - "storesManagerPinnedKeyHint": "Only modules signed with this key are accepted from this source. Paste the publisher's public key (PEM) — used instead of the built-in vendor key.", + "storesManagerPinnedKeyLabel": "Pinned public key — PEM (optional)", + "storesManagerPinnedKeyHint": "Signed modules from this store verify against this key instead of the built-in vendor key. Paste a PEM public key.", "storesManagerNotAvailable": "Not available yet", "storesManagerNotAvailableHint": "The provider hasn't published this store index yet. It will become installable once they do.", "storesSuggestedReclaimDesc": "Recl∆Im — modules for law & duty reform-mapping", @@ -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 straightforward workstation on this machine — no signature requirement, no write-protected audit log.", + "setupScenTryingOutSub": "A relaxed workspace on this machine — no signing, no audit locking.", "setupScenTeamHub": "A shared team hub", "setupScenTeamHubSub": "A secured hub for several people — signed modules, resource limits.", "setupScenRegulated": "Regulated production", @@ -1786,53 +1786,7 @@ "setupApplied": "Done — Ch∆In is set up.", "setupNextTitle": "Next steps", "setupNextHubStart": "Start the hub", - "setupHubRunning": "The hub is running.", - "setupStartHubFirst": "Start the hub first — then the modules can be installed right here.", - "setupActionInstall": "Install {module}", - "@setupActionInstall": {"placeholders": {"module": {"type": "String"}}}, - "setupActionInstalled": "{module} is installed.", - "@setupActionInstalled": {"placeholders": {"module": {"type": "String"}}}, - "setupActionOpenFlow": "Open the “{flow}” example flow", - "@setupActionOpenFlow": {"placeholders": {"flow": {"type": "String"}}}, - "setupModulesOfflineHint": "The modules arrive as an offline bundle, without internet — the documentation explains this under “running without internet”.", - "setupModulesSignedHint": "The modules come from your own, signed source — not from the public store.", - "setupSigPublicStoreNotice": "This setup only accepts signed modules. The public store currently ships unsigned bundles — the prepared modules therefore come from your own, signed source.", - "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}).", - "@setupFreeTextPrivacyLocal": {"placeholders": {"model": {"type": "String"}}}, - "setupFreeTextPrivacyRemote": "Your description is sent to the configured system AI: {model} ({provider}).", - "@setupFreeTextPrivacyRemote": {"placeholders": {"model": {"type": "String"}, "provider": {"type": "String"}}}, - "setupFreeTextUnavailable": "The free-text path needs a configured system AI (Settings → System AI). The choices above always work — no AI required.", - "setupFreeTextParseError": "The system AI did not return a usable suggestion. Pick from the menu above — or try again.", - "setupReflectionTitle": "This is how I read your task", - "setupReflectionScenario": "What it is about: {label}", - "@setupReflectionScenario": {"placeholders": {"label": {"type": "String"}}}, - "setupReflectionIntent": "First task: {label}", - "@setupReflectionIntent": {"placeholders": {"label": {"type": "String"}}}, - "setupReflectionTarget": "Environment: {label}", - "@setupReflectionTarget": {"placeholders": {"label": {"type": "String"}}}, - "setupReflectionApproval": "A human is asked for approval before every AI step.", - "setupReflectionDataLocal": "Data never leaves the building.", - "setupReflectionEditHint": "Nothing is changed without your confirmation. Adjust any point, or go straight to the preview.", - "setupReflectionAdjust": "Adjust", - "setupReflectionToPreview": "To the preview", - "setupProfileDev": "straightforward workstation", - "setupProfileEnterprise": "hardened team operation", - "setupProfileAirgapped": "fully local, regulated operation", - "welcomeChecklistSetupDone": "The setup assistant already took care of the basics ({profile}) — this list shows what has actually happened on your hub.", - "@welcomeChecklistSetupDone": {"placeholders": {"profile": {"type": "String"}}} + "setupNextInstall": "Install the modules", + "setupNextRunFlow": "Run the example flow", + "setupStartCta": "Get started in 3 questions" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f90f664..e0be5a4 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1685,13 +1685,13 @@ abstract class AppLocalizations { /// No description provided for @storesManagerPinnedKeyLabel. /// /// In en, this message translates to: - /// **'Pin a signing key (PEM, optional)'** + /// **'Pinned public key — PEM (optional)'** String get storesManagerPinnedKeyLabel; /// No description provided for @storesManagerPinnedKeyHint. /// /// In en, this message translates to: - /// **'Only modules signed with this key are accepted from this source. Paste the publisher\'s public key (PEM) — used instead of the built-in vendor key.'** + /// **'Signed modules from this store verify against this key instead of the built-in vendor key. Paste a PEM public key.'** String get storesManagerPinnedKeyHint; /// No description provided for @storesManagerNotAvailable. @@ -5092,7 +5092,7 @@ abstract class AppLocalizations { /// No description provided for @setupScenTryingOutSub. /// /// In en, this message translates to: - /// **'A straightforward workstation on this machine — no signature requirement, no write-protected audit log.'** + /// **'A relaxed workspace on this machine — no signing, no audit locking.'** String get setupScenTryingOutSub; /// No description provided for @setupScenTeamHub. @@ -5329,239 +5329,23 @@ abstract class AppLocalizations { /// **'Start the hub'** String get setupNextHubStart; - /// No description provided for @setupHubRunning. + /// No description provided for @setupNextInstall. /// /// In en, this message translates to: - /// **'The hub is running.'** - String get setupHubRunning; + /// **'Install the modules'** + String get setupNextInstall; - /// No description provided for @setupStartHubFirst. + /// No description provided for @setupNextRunFlow. /// /// In en, this message translates to: - /// **'Start the hub first — then the modules can be installed right here.'** - String get setupStartHubFirst; - - /// No description provided for @setupActionInstall. - /// - /// In en, this message translates to: - /// **'Install {module}'** - String setupActionInstall(String module); - - /// No description provided for @setupActionInstalled. - /// - /// In en, this message translates to: - /// **'{module} is installed.'** - String setupActionInstalled(String module); - - /// No description provided for @setupActionOpenFlow. - /// - /// In en, this message translates to: - /// **'Open the “{flow}” example flow'** - String setupActionOpenFlow(String flow); - - /// No description provided for @setupModulesOfflineHint. - /// - /// In en, this message translates to: - /// **'The modules arrive as an offline bundle, without internet — the documentation explains this under “running without internet”.'** - String get setupModulesOfflineHint; - - /// No description provided for @setupModulesSignedHint. - /// - /// In en, this message translates to: - /// **'The modules come from your own, signed source — not from the public store.'** - String get setupModulesSignedHint; - - /// No description provided for @setupSigPublicStoreNotice. - /// - /// In en, this message translates to: - /// **'This setup only accepts signed modules. The public store currently ships unsigned bundles — the prepared modules therefore come from your own, signed source.'** - String get setupSigPublicStoreNotice; - - /// No description provided for @setupAllowUnsigned. - /// - /// In en, this message translates to: - /// **'Allow installing from the public store'** - String get setupAllowUnsigned; - - /// No description provided for @setupAllowUnsignedSub. - /// - /// In en, this message translates to: - /// **'Deliberately relaxes the signature requirement. Fine for trying things out — re-enable it for regulated operation.'** - String get setupAllowUnsignedSub; + /// **'Run the example flow'** + String get setupNextRunFlow; /// No description provided for @setupStartCta. /// /// In en, this message translates to: /// **'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: - /// **'e.g.: pre-screen incoming applications and flag incomplete ones'** - String get setupFreeTextHint; - - /// No description provided for @setupFreeTextSuggest. - /// - /// In en, this message translates to: - /// **'Suggest a setup'** - String get setupFreeTextSuggest; - - /// No description provided for @setupFreeTextPrivacyLocal. - /// - /// In en, this message translates to: - /// **'Your description is processed locally on this machine ({model}).'** - String setupFreeTextPrivacyLocal(String model); - - /// No description provided for @setupFreeTextPrivacyRemote. - /// - /// In en, this message translates to: - /// **'Your description is sent to the configured system AI: {model} ({provider}).'** - String setupFreeTextPrivacyRemote(String model, String provider); - - /// No description provided for @setupFreeTextUnavailable. - /// - /// In en, this message translates to: - /// **'The free-text path needs a configured system AI (Settings → System AI). The choices above always work — no AI required.'** - String get setupFreeTextUnavailable; - - /// No description provided for @setupFreeTextParseError. - /// - /// In en, this message translates to: - /// **'The system AI did not return a usable suggestion. Pick from the menu above — or try again.'** - String get setupFreeTextParseError; - - /// No description provided for @setupReflectionTitle. - /// - /// In en, this message translates to: - /// **'This is how I read your task'** - String get setupReflectionTitle; - - /// No description provided for @setupReflectionScenario. - /// - /// In en, this message translates to: - /// **'What it is about: {label}'** - String setupReflectionScenario(String label); - - /// No description provided for @setupReflectionIntent. - /// - /// In en, this message translates to: - /// **'First task: {label}'** - String setupReflectionIntent(String label); - - /// No description provided for @setupReflectionTarget. - /// - /// In en, this message translates to: - /// **'Environment: {label}'** - String setupReflectionTarget(String label); - - /// No description provided for @setupReflectionApproval. - /// - /// In en, this message translates to: - /// **'A human is asked for approval before every AI step.'** - String get setupReflectionApproval; - - /// No description provided for @setupReflectionDataLocal. - /// - /// In en, this message translates to: - /// **'Data never leaves the building.'** - String get setupReflectionDataLocal; - - /// No description provided for @setupReflectionEditHint. - /// - /// In en, this message translates to: - /// **'Nothing is changed without your confirmation. Adjust any point, or go straight to the preview.'** - String get setupReflectionEditHint; - - /// No description provided for @setupReflectionAdjust. - /// - /// In en, this message translates to: - /// **'Adjust'** - String get setupReflectionAdjust; - - /// No description provided for @setupReflectionToPreview. - /// - /// In en, this message translates to: - /// **'To the preview'** - String get setupReflectionToPreview; - - /// No description provided for @setupProfileDev. - /// - /// In en, this message translates to: - /// **'straightforward workstation'** - String get setupProfileDev; - - /// No description provided for @setupProfileEnterprise. - /// - /// In en, this message translates to: - /// **'hardened team operation'** - String get setupProfileEnterprise; - - /// No description provided for @setupProfileAirgapped. - /// - /// In en, this message translates to: - /// **'fully local, regulated operation'** - String get setupProfileAirgapped; - - /// No description provided for @welcomeChecklistSetupDone. - /// - /// In en, this message translates to: - /// **'The setup assistant already took care of the basics ({profile}) — this list shows what has actually happened on your hub.'** - String welcomeChecklistSetupDone(String profile); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 79b055a..e78a64b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -25,7 +25,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get guidedSetupIntro => - 'Beantworten Sie drei Fragen — Ch∆In stellt passende Konfiguration, Modul-Set und Beispiel-Flow zusammen.'; + 'Beantworte drei Fragen — Ch∆In stellt passende Konfiguration, Modul-Set und Start-Flow zusammen.'; @override String get guidedSetupScenario => 'Szenario'; @@ -235,32 +235,32 @@ class AppLocalizationsDe extends AppLocalizations { 'Vier Schritte zu einem funktionierenden Hub. Live-Zustand — nach jeder Änderung aktualisieren.'; @override - String get welcomeChecklistAi => 'System-KI konfigurieren'; + String get welcomeChecklistAi => 'System-AI konfigurieren'; @override String get welcomeChecklistAiHint => - 'Einstellungen → System-KI. Wird für die KI-Suche und Fehlererklärungen gebraucht.'; + 'Einstellungen → System-AI. Wird für die KI-Suche und Fehlererklärungen gebraucht.'; @override String get welcomeChecklistMcp => 'Öffentliche Capability-Quelle hinzufügen'; @override String get welcomeChecklistMcpHint => - 'Verbindet einen öffentlichen Werkzeug-Katalog (MCP) — dessen Bausteine erscheinen dann im Store. Dort hinzufügen oder unter Einstellungen → MCP-Clients.'; + 'Im Store eine MCP-Quelle hinzufügen oder unter Einstellungen → MCP-Clients konfigurieren.'; @override String get welcomeChecklistModule => 'Ein Text-Modul installieren'; @override String get welcomeChecklistModuleHint => - 'Im Store ein Modul aus der Kategorie Ihrer Wahl installieren. Module sind isolierte (sandboxed) Ch∆In-Komponenten.'; + 'Im Store ein Modul aus der Kategorie deiner Wahl installieren. Module sind 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; ein eigener funktioniert genauso.'; + 'Flows → einen wählen → Starten. Studio bringt einige Demo-Flows mit; auch ein eigener tut\'s.'; @override String get welcomeChecklistAllDone => 'Alle vier Schritte erledigt.'; @@ -869,7 +869,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get storesManagerIntro => - 'Der Hub führt die Module aller Stores unten zu einem Index zusammen. Fügen Sie die Index-URL eines Stores hinzu, damit seine Module hier installierbar werden.'; + 'Der Hub führt die Module aller Stores unten zu einem Index zusammen. Füge die Index-URL eines Stores hinzu, damit seine Module hier installierbar werden.'; @override String get storesManagerAddSection => 'Store hinzufügen'; @@ -907,11 +907,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get storesManagerPinnedKeyLabel => - 'Signatur-Schlüssel anheften (PEM, optional)'; + 'Angepinnter Public-Key — PEM (optional)'; @override String get storesManagerPinnedKeyHint => - 'Nur Module, die mit diesem Schlüssel signiert sind, werden aus dieser Quelle angenommen. Fügen Sie den öffentlichen Schlüssel (PEM) des Herausgebers ein — statt des eingebauten Herstellerschlüssels.'; + 'Signierte Module dieses Stores werden gegen diesen Key geprüft statt gegen den eingebauten Vendor-Key. Füge einen PEM-Public-Key ein.'; @override String get storesManagerNotAvailable => 'Noch nicht verfügbar'; @@ -1619,18 +1619,18 @@ class AppLocalizationsDe extends AppLocalizations { String get maintenanceResetConfirmButton => 'Zurücksetzen'; @override - String get welcomeChecklistAllSetTitle => 'Sie sind eingerichtet.'; + String get welcomeChecklistAllSetTitle => 'Du bist eingerichtet.'; @override String get welcomeChecklistAllSetBody => - 'Drei Stränge, an denen Sie als Nächstes ziehen können:'; + 'Drei Stränge, an denen du als nächstes ziehen kannst:'; @override String get welcomeChecklistNextAuditTitle => 'Audit-Log lesen'; @override String get welcomeChecklistNextAuditBody => - 'Jede Installation, jeder Flow-Lauf, jede Freigabe landet im hash-verketteten Protokoll. Im Protokoll-Tab sehen Sie, was Ihr Hub gemacht hat.'; + 'Jede Installation, jeder Flow-Lauf, jede Freigabe landet im hash-verketteten Log. Im Protokoll-Tab siehst du, was dein Hub gemacht hat.'; @override String get welcomeChecklistNextAuditButton => 'Protokoll öffnen'; @@ -1640,7 +1640,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get welcomeChecklistNextTodayBody => - 'Für Fortgeschrittene: das Skript tools/today/propose.sh regelmäßig (Cron) laufen lassen — die Editorial-Karte im Store füllt sich dann mit kuratierten Stories.'; + 'tools/today/propose.sh per Cron laufen lassen und die Editorial-Karte im Store füllt sich mit operator-kuratierten Stories.'; @override String get welcomeChecklistNextTodayButton => 'Today-Doku öffnen'; @@ -1650,7 +1650,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get welcomeChecklistNextModuleBody => - 'Für Entwickler: Mit `chain new module ` entsteht ein fertiges Modul-Gerüst (Rust+WASM). Das module-sdk übernimmt die technische Anbindung; Sie schreiben nur die eigentliche Logik.'; + 'Mit `chain new module ` ein Rust+WASM-Scaffold erzeugen. Das module-sdk übernimmt die WIT-Plumbing; du schreibst 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 Signaturpflicht, ohne Schreibschutz für das Prüfprotokoll.'; + 'Ein unkomplizierter Arbeitsplatz auf diesem Rechner — ohne Signaturen, ohne Audit-Sperre.'; @override String get setupScenTeamHub => 'Ein Team-Server'; @@ -3138,157 +3138,11 @@ class AppLocalizationsDe extends AppLocalizations { String get setupNextHubStart => 'Hub starten'; @override - String get setupHubRunning => 'Der Hub läuft.'; + String get setupNextInstall => 'Die Module installieren'; @override - String get setupStartHubFirst => - 'Starten Sie zuerst den Hub — dann lassen sich die Module hier installieren.'; - - @override - String setupActionInstall(String module) { - return '$module installieren'; - } - - @override - String setupActionInstalled(String module) { - return '$module ist installiert.'; - } - - @override - String setupActionOpenFlow(String flow) { - return 'Beispiel-Flow „$flow“ öffnen'; - } - - @override - String get setupModulesOfflineHint => - 'Die Module kommen ohne Internet als Offline-Paket auf den Server — die Anleitung dazu steht in der Dokumentation unter „Betrieb ohne Internet“.'; - - @override - String get setupModulesSignedHint => - 'Die Module kommen aus einer eigenen, signierten Quelle — nicht aus dem öffentlichen Store.'; - - @override - String get setupSigPublicStoreNotice => - 'Diese Einrichtung lässt nur signierte Module zu. Der öffentliche Store liefert zurzeit unsignierte Pakete — die vorbereiteten Module kommen daher aus einer eigenen, signierten Quelle.'; - - @override - String get setupAllowUnsigned => - 'Installation aus dem öffentlichen Store erlauben'; - - @override - String get setupAllowUnsignedSub => - 'Lockert die Signaturpflicht bewusst. Gut zum Ausprobieren — für den regulierten Betrieb später wieder aktivieren.'; + String get setupNextRunFlow => 'Den Beispiel-Flow ausführen'; @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'; - - @override - String get setupFreeTextSuggest => 'Vorschlagen lassen'; - - @override - String setupFreeTextPrivacyLocal(String model) { - return 'Ihre Beschreibung wird lokal auf diesem Rechner verarbeitet ($model).'; - } - - @override - String setupFreeTextPrivacyRemote(String model, String provider) { - return 'Ihre Beschreibung wird an die eingerichtete System-KI gesendet: $model ($provider).'; - } - - @override - String get setupFreeTextUnavailable => - 'Für den Freitext-Weg braucht Ch∆In eine eingerichtete System-KI (Einstellungen → System-KI). Die Auswahl oben funktioniert immer — ganz ohne KI.'; - - @override - String get setupFreeTextParseError => - 'Die System-KI hat keinen verwertbaren Vorschlag geliefert. Wählen Sie oben aus dem Menü — oder versuchen Sie es noch einmal.'; - - @override - String get setupReflectionTitle => 'So verstehe ich Ihre Aufgabe'; - - @override - String setupReflectionScenario(String label) { - return 'Worum es geht: $label'; - } - - @override - String setupReflectionIntent(String label) { - return 'Erste Aufgabe: $label'; - } - - @override - String setupReflectionTarget(String label) { - return 'Umgebung: $label'; - } - - @override - String get setupReflectionApproval => - 'Vor jedem KI-Schritt wird ein Mensch um Freigabe gebeten.'; - - @override - String get setupReflectionDataLocal => 'Daten bleiben vollständig im Haus.'; - - @override - String get setupReflectionEditHint => - 'Es wird nichts ohne Ihre Bestätigung geändert. Passen Sie die Punkte an oder gehen Sie direkt zur Vorschau.'; - - @override - String get setupReflectionAdjust => 'Anpassen'; - - @override - String get setupReflectionToPreview => 'Zur Vorschau'; - - @override - String get setupProfileDev => 'unkomplizierter Arbeitsplatz'; - - @override - String get setupProfileEnterprise => 'abgesicherter Team-Betrieb'; - - @override - String get setupProfileAirgapped => - 'vollständig lokaler, regulierter Betrieb'; - - @override - String welcomeChecklistSetupDone(String profile) { - return 'Die Grundeinrichtung ($profile) hat der Einrichtungs-Assistent bereits erledigt — hier sehen Sie, was auf dem Hub schon passiert ist.'; - } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a1425a0..6a7d777 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -925,11 +925,12 @@ class AppLocalizationsEn extends AppLocalizations { String get storesManagerAddShort => 'Add'; @override - String get storesManagerPinnedKeyLabel => 'Pin a signing key (PEM, optional)'; + String get storesManagerPinnedKeyLabel => + 'Pinned public key — PEM (optional)'; @override String get storesManagerPinnedKeyHint => - 'Only modules signed with this key are accepted from this source. Paste the publisher\'s public key (PEM) — used instead of the built-in vendor key.'; + 'Signed modules from this store verify against this key instead of the built-in vendor key. Paste a PEM public key.'; @override String get storesManagerNotAvailable => 'Not available yet'; @@ -2992,7 +2993,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get setupScenTryingOutSub => - 'A straightforward workstation on this machine — no signature requirement, no write-protected audit log.'; + 'A relaxed workspace on this machine — no signing, no audit locking.'; @override String get setupScenTeamHub => 'A shared team hub'; @@ -3135,155 +3136,11 @@ class AppLocalizationsEn extends AppLocalizations { String get setupNextHubStart => 'Start the hub'; @override - String get setupHubRunning => 'The hub is running.'; + String get setupNextInstall => 'Install the modules'; @override - String get setupStartHubFirst => - 'Start the hub first — then the modules can be installed right here.'; - - @override - String setupActionInstall(String module) { - return 'Install $module'; - } - - @override - String setupActionInstalled(String module) { - return '$module is installed.'; - } - - @override - String setupActionOpenFlow(String flow) { - return 'Open the “$flow” example flow'; - } - - @override - String get setupModulesOfflineHint => - 'The modules arrive as an offline bundle, without internet — the documentation explains this under “running without internet”.'; - - @override - String get setupModulesSignedHint => - 'The modules come from your own, signed source — not from the public store.'; - - @override - String get setupSigPublicStoreNotice => - 'This setup only accepts signed modules. The public store currently ships unsigned bundles — the prepared modules therefore come from your own, signed source.'; - - @override - String get setupAllowUnsigned => 'Allow installing from the public store'; - - @override - String get setupAllowUnsignedSub => - 'Deliberately relaxes the signature requirement. Fine for trying things out — re-enable it for regulated operation.'; + String get setupNextRunFlow => 'Run the example flow'; @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'; - - @override - String get setupFreeTextSuggest => 'Suggest a setup'; - - @override - String setupFreeTextPrivacyLocal(String model) { - return 'Your description is processed locally on this machine ($model).'; - } - - @override - String setupFreeTextPrivacyRemote(String model, String provider) { - return 'Your description is sent to the configured system AI: $model ($provider).'; - } - - @override - String get setupFreeTextUnavailable => - 'The free-text path needs a configured system AI (Settings → System AI). The choices above always work — no AI required.'; - - @override - String get setupFreeTextParseError => - 'The system AI did not return a usable suggestion. Pick from the menu above — or try again.'; - - @override - String get setupReflectionTitle => 'This is how I read your task'; - - @override - String setupReflectionScenario(String label) { - return 'What it is about: $label'; - } - - @override - String setupReflectionIntent(String label) { - return 'First task: $label'; - } - - @override - String setupReflectionTarget(String label) { - return 'Environment: $label'; - } - - @override - String get setupReflectionApproval => - 'A human is asked for approval before every AI step.'; - - @override - String get setupReflectionDataLocal => 'Data never leaves the building.'; - - @override - String get setupReflectionEditHint => - 'Nothing is changed without your confirmation. Adjust any point, or go straight to the preview.'; - - @override - String get setupReflectionAdjust => 'Adjust'; - - @override - String get setupReflectionToPreview => 'To the preview'; - - @override - String get setupProfileDev => 'straightforward workstation'; - - @override - String get setupProfileEnterprise => 'hardened team operation'; - - @override - String get setupProfileAirgapped => 'fully local, regulated operation'; - - @override - String welcomeChecklistSetupDone(String profile) { - return 'The setup assistant already took care of the basics ($profile) — this list shows what has actually happened on your hub.'; - } } diff --git a/lib/pages/welcome.dart b/lib/pages/welcome.dart index 985f665..144b52f 100644 --- a/lib/pages/welcome.dart +++ b/lib/pages/welcome.dart @@ -22,44 +22,9 @@ import '../theme/tokens.dart'; import '../widgets/guided_setup_dialog.dart'; import '../widgets/widgets.dart'; -class WelcomePage extends StatefulWidget { +class WelcomePage extends StatelessWidget { const WelcomePage({super.key}); - @override - State createState() => _WelcomePageState(); -} - -class _WelcomePageState extends State { - /// Bumped when the guided-setup dialog closes so the onboarding - /// checklist remounts and re-probes — the assistant may have just - /// installed the modules the checklist looks for. - int _checklistEpoch = 0; - - /// One auto-open per app run: a fresh install that dismisses the - /// wizard should not have it spring back on every rebuild. - static bool _autoOpenedThisRun = false; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAutoOpen()); - } - - /// On a fresh hub (no config, no recorded setup plan) the wizard - /// IS the onboarding — open it rather than hoping the operator - /// finds the button. Once a setup exists it steps back for good. - Future _maybeAutoOpen() async { - if (_autoOpenedThisRun || !mounted) return; - if (!GuidedSetupDialog.isFreshInstall()) return; - _autoOpenedThisRun = true; - await _openSetup(); - } - - Future _openSetup() async { - await GuidedSetupDialog.show(context); - if (mounted) setState(() => _checklistEpoch++); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -100,12 +65,11 @@ class _WelcomePageState extends State { alignment: Alignment.centerLeft, // High-emphasis: the guided setup is the fastest path // to a working first run, so lead with a filled button - // rather than a low-key outlined one. Honest framing: - // "get started in 3 questions", not tool-speak. + // rather than a low-key outlined one. child: FilledButton.icon( - onPressed: _openSetup, + onPressed: () => GuidedSetupDialog.show(context), icon: const Icon(Icons.auto_fix_high), - label: Text(l.setupStartCta), + label: Text(AppLocalizations.of(context)!.guidedSetupTitle), ), ), const SizedBox(height: ChainSpace.xxl), @@ -117,7 +81,7 @@ class _WelcomePageState extends State { const _HubDownHero(), const SizedBox(height: ChainSpace.xxl), ] else - _OnboardingChecklist(key: ValueKey(_checklistEpoch)), + const _OnboardingChecklist(), const _PillarRow(), const SizedBox(height: ChainSpace.xxl), const _SectionLabel(textKey: _SectionLabelKey.trust), @@ -551,7 +515,7 @@ const String _kChecklistDismissedKey = 'welcome.checklist.dismissed'; /// parallel; failures stay false and the checklist still /// renders (with the "Refresh" button for retry). class _OnboardingChecklist extends StatefulWidget { - const _OnboardingChecklist({super.key}); + const _OnboardingChecklist(); @override State<_OnboardingChecklist> createState() => _OnboardingChecklistState(); @@ -566,12 +530,6 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> { bool _moduleOk = false; bool _flowOk = false; - /// Profile the guided setup applied (from `setup-plan.yaml`), or - /// null when the assistant never ran. Connects the two onboarding - /// surfaces: the checklist says what the assistant already covered - /// instead of ignoring it. - String? _setupProfile; - @override void initState() { super.initState(); @@ -583,7 +541,6 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> { if (!mounted) return; setState(() { _dismissed = prefs.getBool(_kChecklistDismissedKey) ?? false; - _setupProfile = GuidedSetupDialog.appliedSetupProfile(); _initialised = true; }); if (!_dismissed) { @@ -641,13 +598,6 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> { setState(() => _dismissed = true); } - /// Localized plain-language label for the applied setup profile. - String _profileLabel(AppLocalizations l) => switch (_setupProfile) { - 'enterprise' => l.setupProfileEnterprise, - 'air-gapped' => l.setupProfileAirgapped, - _ => l.setupProfileDev, - }; - @override Widget build(BuildContext context) { if (!_initialised || _dismissed) return const SizedBox.shrink(); @@ -724,27 +674,6 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> { color: theme.colorScheme.onSurfaceVariant, ), ), - // The guided setup already ran: say so, so the checklist - // reads as "what's left", not as a second, disconnected - // onboarding that ignores the assistant's work. - if (_setupProfile != null) ...[ - const SizedBox(height: ChainSpace.sm), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.check_circle, size: 16, color: ChainColors.success), - const SizedBox(width: 6), - Expanded( - child: Text( - l.welcomeChecklistSetupDone(_profileLabel(l)), - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ], const SizedBox(height: ChainSpace.md), Container( decoration: BoxDecoration( diff --git a/lib/widgets/guided_setup_dialog.dart b/lib/widgets/guided_setup_dialog.dart index 2ea36d8..a5dd4e6 100644 --- a/lib/widgets/guided_setup_dialog.dart +++ b/lib/widgets/guided_setup_dialog.dart @@ -9,74 +9,15 @@ // plain-language summary — never the CLI's English prose. Every // option carries a one-line explanation of what it configures. -import 'dart:async' show unawaited; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import '../data/error_presentation.dart'; -import '../data/hub.dart'; 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 -/// hallucinated enum can never reach the engine. -const _kScenarioValues = [ - 'trying-out', - 'team-hub', - 'regulated-production', - 'building-modules', -]; -const _kIntentValues = [ - 'hello-world', - 'extract-text', - 'extract-summarize', - 'classify-documents', - 'build-module', -]; -const _kTargetValues = [ - 'this-laptop', - 'home-server', - 'air-gapped-server', - 'container', -]; - -/// Extract + validate the system AI's setup suggestion: the first -/// JSON object in [text] with valid enum values for all three -/// answers. Returns null when nothing safe could be parsed. -@visibleForTesting -Map? parseSetupSuggestion(String text) { - final start = text.indexOf('{'); - final end = text.lastIndexOf('}'); - if (start < 0 || end <= start) return null; - final Object? decoded; - try { - decoded = jsonDecode(text.substring(start, end + 1)); - } on FormatException { - return null; - } - if (decoded is! Map) return null; - final scenario = decoded['scenario']; - final intent = decoded['intent']; - final target = decoded['target']; - if (!_kScenarioValues.contains(scenario) || - !_kIntentValues.contains(intent) || - !_kTargetValues.contains(target)) { - return null; - } - return { - 'scenario': scenario, - 'intent': intent, - 'target': target, - 'require_approval': decoded['require_approval'] == true, - 'data_must_stay_local': decoded['data_must_stay_local'] == true, - }; -} /// A selectable setup option: the stable kebab wire value plus the /// localized label + one-line explanation resolved at build time. @@ -88,80 +29,12 @@ class _Option { } class GuidedSetupDialog extends StatefulWidget { - /// The shell, resolved by [show] before the dialog route detaches - /// from the shell's subtree. Drives the post-apply "open the - /// starter flow" navigation; null in tests. - final StudioShellState? shell; + const GuidedSetupDialog({super.key}); - /// Test seam: start on the review step with this parsed plan - /// instead of shelling out to `chain init --plan-json`. - @visibleForTesting - final Map? debugPlan; - - /// Test seam: combined with [debugPlan], start in the applied - /// (next-steps) state. - @visibleForTesting - final bool debugApplied; - - /// Test seam: skip the live system-AI probe so the free-text - /// section renders its deterministic "no AI configured" state. - @visibleForTesting - final bool debugSkipAiProbe; - - const GuidedSetupDialog({ - super.key, - this.shell, - this.debugPlan, - this.debugApplied = false, - this.debugSkipAiProbe = false, - }); - - static Future show(BuildContext context) { - final shell = StudioShellState.of(context); - return showDialog( - context: context, - builder: (_) => GuidedSetupDialog(shell: shell), - ); - } - - /// Path of the operator dir (`~/.chain`), or null when no home - /// directory can be resolved. - static String? _chainDir() { - final home = - Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; - if (home == null) return null; - return '$home${Platform.pathSeparator}.chain'; - } - - /// True on a fresh install: neither a config nor a recorded setup - /// plan exists yet. Drives the welcome page's auto-open. - static bool isFreshInstall() { - final dir = _chainDir(); - if (dir == null) return false; - final sep = Platform.pathSeparator; - return !File('$dir${sep}config.yaml').existsSync() && - !File('$dir${sep}setup-plan.yaml').existsSync(); - } - - /// The `profile:` of the recorded setup plan (`setup-plan.yaml`), - /// or null when the guided setup never applied one. Lets the - /// onboarding checklist say "the assistant covered this" instead - /// of presenting a second, disconnected onboarding surface. - static String? appliedSetupProfile() { - final dir = _chainDir(); - if (dir == null) return null; - final f = File('$dir${Platform.pathSeparator}setup-plan.yaml'); - if (!f.existsSync()) return null; - try { - for (final line in f.readAsLinesSync()) { - final m = RegExp(r'^profile:\s*(\S+)').firstMatch(line.trim()); - if (m != null) return m.group(1); - } - } on FileSystemException { - return null; - } - return null; - } + static Future show(BuildContext context) => showDialog( + context: context, + builder: (_) => const GuidedSetupDialog(), + ); @override State createState() => _GuidedSetupDialogState(); @@ -198,89 +71,19 @@ class _GuidedSetupDialogState extends State { String _target = 'this-laptop'; bool _requireApproval = false; bool _dataLocal = false; - bool _allowUnsigned = false; bool _busy = false; Map? _plan; // parsed SetupPlan from --plan-json bool _applied = false; - // Post-apply action state: hub reachability + per-module install - // progress, so the next steps are buttons that report back instead - // of CLI commands to retype. - bool _hubUp = false; - bool _startingHub = false; - final Set _installing = {}; - final Set _installed = {}; - - // Free-text (AI) path state. The menu path is always complete on - // its own; the AI merely PRE-SELECTS answers for review — it never - // applies anything (trust rule from guided-setup.md). - final TextEditingController _goalCtl = TextEditingController(); - SystemAiStatus? _aiStatus; // null until probed / when unreachable - bool _aiProbed = false; - bool _suggestBusy = false; - bool _showReflection = false; - - @override - void initState() { - super.initState(); - final seeded = widget.debugPlan; - if (seeded != null) { - _plan = seeded; - _step = _totalSteps; - _applied = widget.debugApplied; - _aiProbed = true; // tests: skip the live probe - } else if (widget.debugSkipAiProbe) { - _aiProbed = true; - } else { - unawaited(_probeAi()); - } - } - - @override - void dispose() { - _goalCtl.dispose(); - try { - _answersDir?.deleteSync(recursive: true); - } on FileSystemException { - // Best-effort cleanup; the OS temp reaper covers the rest. - } - super.dispose(); - } - - /// Probe whether a system AI is configured — the free-text path - /// needs one; without it the menu path stands alone (no dead end). - Future _probeAi() async { - SystemAiStatus? status; - try { - status = await HubService.instance.systemAiStatus(); - } catch (_) { - status = null; - } - if (!mounted) return; - setState(() { - _aiStatus = status; - _aiProbed = true; - }); - } - String _answersYaml() => 'scenario: $_scenario\n' 'intent: $_intent\n' 'target: $_target\n' 'require_approval: $_requireApproval\n' - '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; + 'data_must_stay_local: $_dataLocal\n'; Future _writeAnswers() async { - _answersDir ??= await Directory.systemTemp.createTemp('chain-setup-'); - final f = File( - '${_answersDir!.path}${Platform.pathSeparator}answers.yaml', - ); + final f = File('${Directory.systemTemp.path}/chain-setup-answers.yaml'); await f.writeAsString(_answersYaml()); return f.path; } @@ -318,11 +121,6 @@ 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(); @@ -332,147 +130,22 @@ class _GuidedSetupDialogState extends State { if (!mounted) return; setState(() => _busy = false); if (r.ok) { - final warnings = r.stderr - .split('\n') - .where((line) => line.toLowerCase().contains('warn')) - .join('\n') - .trim(); - setState(() { - _applied = true; - _applyWarnings = warnings; - }); - unawaited(_probeHub()); + setState(() => _applied = true); } else { showFaiProcessError(context, 'chain init --apply', r.stdout, r.stderr); } } - /// Refresh the "is the hub reachable" signal driving the - /// post-apply action rows. - Future _probeHub() async { - final up = await HubService.instance - .healthy() - .catchError((Object _) => false); - if (mounted) setState(() => _hubUp = up); - } - - /// Start the local daemon, then poll until it answers (the start - /// command returns before the gRPC endpoint is up). A failed start - /// often just means "already running", so probe before reporting. - Future _startHub() async { - setState(() => _startingHub = true); - final r = await SystemActions.chainDaemon(['start']); - var up = false; - for (var i = 0; i < 6 && !up; i++) { - await Future.delayed(const Duration(milliseconds: 700)); - up = await HubService.instance - .healthy() - .catchError((Object _) => false); - } - if (!mounted) return; - setState(() { - _startingHub = false; - _hubUp = up; - }); - if (!up) { - await showFaiProcessErrorDialog( - context, - 'chain daemon start', - r.stdout, - r.stderr, - ); - } - } - - /// Structured prompt for the system AI. English, fixed shape, and - /// the reply is validated against the enum whitelists — the model - /// only ever pre-selects menu answers, it cannot inject config. - String _suggestionPrompt(String goal) => - 'You configure the Ch∆In workflow platform. Map the operator\'s ' - 'goal to setup answers.\n' - 'Goal: """$goal"""\n' - 'Reply with ONLY one JSON object, no prose, of this exact shape:\n' - '{"scenario":"trying-out|team-hub|regulated-production|building-modules",' - '"intent":"hello-world|extract-text|extract-summarize|classify-documents|build-module",' - '"target":"this-laptop|home-server|air-gapped-server|container",' - '"require_approval":true|false,"data_must_stay_local":true|false}\n' - 'Pick the closest match for each field.'; - - /// Ask the system AI to map the free-text goal onto the menu - /// answers, then show the editable reflection. Never applies — - /// the suggestion only pre-selects; preview + apply stay manual. - Future _suggest() async { - final goal = _goalCtl.text.trim(); - if (goal.isEmpty) return; - setState(() => _suggestBusy = true); - final l = AppLocalizations.of(context)!; - try { - final r = await HubService.instance.askAi(_suggestionPrompt(goal)); - if (!mounted) return; - setState(() => _suggestBusy = false); - if (r.errorKind.isNotEmpty) { - await showChainErrorDialog(context, 'system-ai', r.text); - return; - } - final parsed = parseSetupSuggestion(r.text); - if (parsed == null) { - await showChainErrorDialog( - context, - 'system-ai', - '${l.setupFreeTextParseError}\n\n${r.text}', - ); - return; - } - setState(() { - _scenario = parsed['scenario'] as String; - _intent = parsed['intent'] as String; - _target = parsed['target'] as String; - _requireApproval = parsed['require_approval'] as bool; - _dataLocal = parsed['data_must_stay_local'] as bool; - _showReflection = true; - }); - } catch (e) { - if (!mounted) return; - setState(() => _suggestBusy = false); - await showChainErrorDialog(context, 'system-ai', e); - } - } - - /// Install one plan module by capability name — the hub resolves - /// the bundle URL from its store index. - Future _install(String module) async { - setState(() => _installing.add(module)); - try { - await HubService.instance.installModule(source: module); - if (mounted) setState(() => _installed.add(module)); - } catch (e) { - if (mounted) { - await showChainErrorDialog(context, 'install $module', e); - } - } finally { - if (mounted) setState(() => _installing.remove(module)); - } - } - @override Widget build(BuildContext context) { final l = AppLocalizations.of(context)!; final reviewing = _step >= _totalSteps; - final title = reviewing - ? l.setupReviewTitle - : _showReflection - ? l.setupReflectionTitle - : _stepTitle(l); return AlertDialog( - title: Text(title), + title: Text(reviewing ? l.setupReviewTitle : _stepTitle(l)), content: SizedBox( width: 480, child: SingleChildScrollView( - child: reviewing - ? _reviewStep(l) - : _showReflection - ? _reflectionView(l) - : _answerStep(l), + child: reviewing ? _reviewStep(l) : _answerStep(l), ), ), actions: _actions(l), @@ -528,112 +201,6 @@ class _GuidedSetupDialogState extends State { onChanged: (v) => setState(() => _dataLocal = v), ), ], - // The free-text alternative lives on the first step: describe - // the goal, the system AI pre-selects the menu answers. Menu - // path always stands alone (air-gap-safe, no dead end). - if (_step == 0 && _aiProbed) ...[ - const SizedBox(height: ChainSpace.md), - Text( - l.setupChooseFreeText, - style: Theme.of(context).textTheme.labelLarge, - ), - const SizedBox(height: ChainSpace.sm), - if (_aiStatus?.enabled == true) ...[ - TextField( - controller: _goalCtl, - minLines: 2, - maxLines: 4, - decoration: InputDecoration( - hintText: l.setupFreeTextHint, - border: const OutlineInputBorder(), - ), - ), - const SizedBox(height: ChainSpace.xs), - // Trust rule: be transparent about where the description - // goes before the operator types anything sensitive. - Text( - _aiIsLocal() - ? l.setupFreeTextPrivacyLocal(_aiStatus?.model ?? '') - : l.setupFreeTextPrivacyRemote( - _aiStatus?.model ?? '', - _aiStatus?.provider ?? '', - ), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: ChainSpace.sm), - Align( - alignment: Alignment.centerLeft, - child: FilledButton.tonalIcon( - onPressed: _suggestBusy ? null : _suggest, - icon: _suggestBusy - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.auto_awesome, size: 18), - label: Text(l.setupFreeTextSuggest), - ), - ), - ] else - _hintRow(l.setupFreeTextUnavailable), - ], - ], - ); - } - - /// True when the configured system AI runs on this machine — the - /// privacy line says so instead of naming a cloud provider. - bool _aiIsLocal() { - final s = _aiStatus; - if (s == null) return false; - return s.provider == 'ollama' || - s.endpoint.contains('localhost') || - s.endpoint.contains('127.0.0.1'); - } - - /// Editable reflection of the AI suggestion: "this is how I read - /// your task", in the same localized labels the menu uses. The - /// operator either adjusts (steps, pre-selected) or goes straight - /// to the same preview the menu path uses. Nothing auto-applies. - Widget _reflectionView(AppLocalizations l) { - final theme = Theme.of(context); - String labelFor(List<_Option> opts, String value) => - opts.firstWhere((o) => o.value == value, orElse: () => opts.first) - .label(l); - final lines = [ - l.setupReflectionScenario(labelFor(_scenarios, _scenario)), - l.setupReflectionIntent(labelFor(_intents, _intent)), - l.setupReflectionTarget(labelFor(_targets, _target)), - if (_requireApproval) l.setupReflectionApproval, - if (_dataLocal) l.setupReflectionDataLocal, - ]; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (final line in lines) - Padding( - padding: const EdgeInsets.only(bottom: 6), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('· ', style: theme.textTheme.bodyMedium), - Expanded( - child: Text(line, style: theme.textTheme.bodyMedium), - ), - ], - ), - ), - const SizedBox(height: ChainSpace.xs), - Text( - l.setupReflectionEditHint, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), ], ); } @@ -643,16 +210,26 @@ class _GuidedSetupDialogState extends State { if (plan == null) return const SizedBox.shrink(); final theme = Theme.of(context); final lines = _planLines(l, plan); - final modules = (plan['modules'] as List?)?.cast() ?? const []; - final airGapped = (plan['profile'] as String? ?? '') == 'air-gapped'; - final sigGate = plan['require_signatures'] == true; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (_applied) - ..._appliedSteps(l, plan) - else ...[ + if (_applied) ...[ + Row( + children: [ + Icon(Icons.check_circle, color: ChainColors.success, size: 20), + const SizedBox(width: ChainSpace.sm), + Text(l.setupApplied, style: theme.textTheme.titleSmall), + ], + ), + const SizedBox(height: ChainSpace.md), + Text(l.setupNextTitle, style: theme.textTheme.labelLarge), + const SizedBox(height: 4), + _next(l.setupNextHubStart), + if ((plan['modules'] as List?)?.isNotEmpty ?? false) + _next(l.setupNextInstall), + _next(l.setupNextRunFlow), + ] else ...[ for (final line in lines) Padding( padding: const EdgeInsets.only(bottom: 6), @@ -666,44 +243,6 @@ class _GuidedSetupDialogState extends State { ], ), ), - // Signature dead end (SEC-01): a regulated connected plan - // refuses the public store's unsigned bundles — the very - // installs suggested after apply. Explain it and offer the - // relaxation as one deliberate, reversible switch. The plan - // is re-assembled on toggle so the preview stays truthful. - if (!airGapped && modules.isNotEmpty && (sigGate || _allowUnsigned)) ...[ - const SizedBox(height: ChainSpace.sm), - if (sigGate) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.verified_user_outlined, - size: 16, - color: theme.colorScheme.tertiary, - ), - const SizedBox(width: 6), - Expanded( - child: Text( - l.setupSigPublicStoreNotice, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.tertiary, - ), - ), - ), - ], - ), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text(l.setupAllowUnsigned), - subtitle: Text(l.setupAllowUnsignedSub), - value: _allowUnsigned, - onChanged: (v) { - setState(() => _allowUnsigned = v); - _goReview(); - }, - ), - ], if (_configExists()) ...[ const SizedBox(height: ChainSpace.sm), Row( @@ -727,147 +266,13 @@ class _GuidedSetupDialogState extends State { ); } - /// Post-apply next steps as real Studio actions: start the hub, - /// install each plan module, open the starter flow — no CLI text. - List _appliedSteps(AppLocalizations l, Map plan) { - final theme = Theme.of(context); - final modules = (plan['modules'] as List?)?.cast() ?? const []; - final airGapped = (plan['profile'] as String? ?? '') == 'air-gapped'; - final sigGate = plan['require_signatures'] == true; - final flow = plan['starter_flow'] as String? ?? ''; - return [ - Row( - children: [ - Icon(Icons.check_circle, color: ChainColors.success, size: 20), - const SizedBox(width: ChainSpace.sm), - Text(l.setupApplied, style: theme.textTheme.titleSmall), - ], - ), - const SizedBox(height: ChainSpace.md), - Text(l.setupNextTitle, style: theme.textTheme.labelLarge), - const SizedBox(height: ChainSpace.sm), - // 1 — the hub. Everything else needs it. - if (_hubUp) - _doneRow(l.setupHubRunning) - else - _actionRow( - FilledButton.tonalIcon( - onPressed: _startingHub ? null : _startHub, - icon: _startingHub - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.play_arrow, size: 18), - label: Text(l.setupNextHubStart), - ), - ), - // 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), - _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) - ? _doneRow(l.setupActionInstalled(m)) - : _actionRow( - FilledButton.tonalIcon( - onPressed: (!_hubUp || _installing.contains(m)) - ? null - : () => _install(m), - icon: _installing.contains(m) - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.download, size: 18), - label: Text(l.setupActionInstall(m)), - ), - ), - ], - ], - // 3 — the starter flow (auto-imported on first hub start). - if (flow.isNotEmpty) - _actionRow( - OutlinedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - widget.shell?.navigateTo('flows'); - }, - icon: const Icon(Icons.account_tree_outlined, size: 18), - 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, - ), - ), - ], - ]; - } - - Widget _actionRow(Widget child) => Padding( - padding: const EdgeInsets.only(top: ChainSpace.xs), - child: Align(alignment: Alignment.centerLeft, child: child), - ); - - Widget _doneRow(String label) => Padding( - padding: const EdgeInsets.only(top: ChainSpace.xs), + Widget _next(String label) => Padding( + padding: const EdgeInsets.only(top: 4), child: Row( children: [ - Icon(Icons.check_circle, size: 18, color: ChainColors.success), - const SizedBox(width: 6), - Expanded( - child: Text(label, style: Theme.of(context).textTheme.bodyMedium), - ), - ], - ), - ); - - Widget _hintRow(String text) => Padding( - padding: const EdgeInsets.only(top: ChainSpace.xs), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.info_outline, - size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 6), - Expanded( - child: Text( - text, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), + Icon(Icons.arrow_right, size: 18, color: Theme.of(context).colorScheme.primary), + const SizedBox(width: 4), + Expanded(child: Text(label, style: Theme.of(context).textTheme.bodyMedium)), ], ), ); @@ -875,9 +280,8 @@ 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 (profile) { + switch (plan['profile'] as String? ?? '') { case 'air-gapped': lines.add(l.setupPlanIntroAirgapped); case 'enterprise': @@ -885,34 +289,13 @@ 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; } @@ -938,23 +321,6 @@ class _GuidedSetupDialogState extends State { ), ]; } - if (_showReflection && _step < _totalSteps) { - // AI reflection: adjust step-by-step (answers stay pre-selected) - // or continue to the same preview the menu path uses. - return [ - TextButton( - onPressed: () => setState(() => _showReflection = false), - child: Text(l.setupReflectionAdjust), - ), - FilledButton( - onPressed: () { - setState(() => _showReflection = false); - _goReview(); - }, - child: Text(l.setupReflectionToPreview), - ), - ]; - } if (_step < _totalSteps) { return [ TextButton( diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 3f39e3f..0a5fc2c 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -13,4 +13,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 -COCOAPODS: 1.17.0 +COCOAPODS: 1.16.2 diff --git a/pubspec.lock b/pubspec.lock index 720d556..3f89ee3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -172,11 +172,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.5" - flutter_driver: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" flutter_highlight: dependency: transitive description: @@ -224,11 +219,6 @@ packages: description: flutter source: sdk version: "0.0.0" - fuchsia_remote_debug_protocol: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" google_cloud: dependency: transitive description: @@ -301,11 +291,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" - integration_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" intl: dependency: "direct main" description: @@ -450,14 +435,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - process: - dependency: transitive - description: - name: process - sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 - url: "https://pub.dev" - source: hosted - version: "5.0.5" protobuf: dependency: transitive description: @@ -567,14 +544,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - sync_http: - dependency: transitive - description: - name: sync_http - sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" - url: "https://pub.dev" - source: hosted - version: "0.3.1" term_glyph: dependency: transitive description: @@ -695,14 +664,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - webdriver: - dependency: transitive - description: - name: webdriver - sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" - url: "https://pub.dev" - source: hosted - version: "3.1.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index edc2065..1a9cf75 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,8 +50,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - integration_test: - sdk: flutter flutter_lints: ^6.0.0 # Local-development override: while editing the swappable flow diff --git a/test/guided_setup_test.dart b/test/guided_setup_test.dart index 7e3e6e6..23d2836 100644 --- a/test/guided_setup_test.dart +++ b/test/guided_setup_test.dart @@ -26,41 +26,6 @@ Widget _host() => MaterialApp( ), ); -/// Host that opens the dialog pre-seeded on the review / applied -/// step with [plan] (the parsed `--plan-json` shape), bypassing the -/// CLI subprocess. -Widget _seededHost(Map plan, {bool applied = false}) => - MaterialApp( - locale: const Locale('de'), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: Builder( - builder: (context) => Scaffold( - body: Center( - child: ElevatedButton( - onPressed: () => showDialog( - context: context, - builder: (_) => - GuidedSetupDialog(debugPlan: plan, debugApplied: applied), - ), - child: const Text('open'), - ), - ), - ), - ), - ); - -Map _regulatedPlan({bool signed = true}) => { - 'profile': 'enterprise', - 'modules': ['text.extract', 'text.summarize'], - 'starter_flow': 'extract-summarize', - 'runbook': 'service', - 'curated_docs': [], - 'require_signatures': signed, - 'worm_audit': false, - 'approval_step': true, -}; - void main() { testWidgets('step 1 shows scenario options in German with explanations', ( tester, @@ -110,183 +75,4 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Schritt 2 von 3'), findsOneWidget); }); - - testWidgets( - 'review of a regulated plan explains the signature dead end and offers the relaxation switch', - (tester) async { - await tester.pumpWidget(_seededHost(_regulatedPlan())); - await tester.tap(find.text('open')); - await tester.pumpAndSettle(); - - // The plain-language notice + the deliberate one-click switch. - expect( - find.textContaining('Der öffentliche Store liefert zurzeit unsignierte Pakete'), - findsOneWidget, - ); - expect( - find.text('Installation aus dem öffentlichen Store erlauben'), - findsOneWidget, - ); - // No raw CLI prose anywhere. - expect(find.textContaining('chain install'), findsNothing); - }, - ); - - testWidgets( - 'applied state renders clickable next steps — start hub, per-module install, open flow', - (tester) async { - await tester.pumpWidget( - _seededHost(_regulatedPlan(signed: false), applied: true), - ); - await tester.tap(find.text('open')); - await tester.pumpAndSettle(); - - expect(find.text('Fertig — Ch∆In ist eingerichtet.'), findsOneWidget); - // Hub not probed up in tests → the start action + the - // install-needs-hub hint show, and install buttons exist - // (disabled) for each plan module. - expect(find.text('Hub starten'), findsOneWidget); - expect( - find.textContaining('Starten Sie zuerst den Hub'), - findsOneWidget, - ); - expect(find.text('text.extract installieren'), findsOneWidget); - expect(find.text('text.summarize installieren'), findsOneWidget); - expect( - find.text('Beispiel-Flow „extract-summarize“ öffnen'), - findsOneWidget, - ); - // No CLI text in the GUI path. - expect(find.textContaining('chain serve'), findsNothing); - expect(find.textContaining('chain install'), findsNothing); - }, - ); - - testWidgets( - '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('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, - ); - }, - ); - - testWidgets( - 'step 1 offers the free-text path with an honest no-AI fallback hint', - (tester) async { - await tester.pumpWidget( - MaterialApp( - locale: const Locale('de'), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - home: Builder( - builder: (context) => Scaffold( - body: Center( - child: ElevatedButton( - onPressed: () => showDialog( - context: context, - builder: (_) => - const GuidedSetupDialog(debugSkipAiProbe: true), - ), - child: const Text('open'), - ), - ), - ), - ), - ), - ); - await tester.tap(find.text('open')); - await tester.pumpAndSettle(); - - expect( - find.text('Oder beschreiben Sie einfach, was Sie vorhaben'), - findsOneWidget, - ); - // No system AI configured → honest fallback: the menu always - // works; no dead end, no disabled mystery button. - expect( - find.textContaining('Die Auswahl oben funktioniert immer'), - findsOneWidget, - ); - expect(find.text('Vorschlagen lassen'), findsNothing); - }, - ); - - group('parseSetupSuggestion', () { - test('accepts a valid JSON object embedded in prose', () { - final r = parseSetupSuggestion( - 'Sure! Here is the mapping:\n' - '{"scenario":"regulated-production","intent":"classify-documents",' - '"target":"home-server","require_approval":true,' - '"data_must_stay_local":false}\n' - 'Let me know if you need anything else.', - ); - expect(r, isNotNull); - expect(r!['scenario'], 'regulated-production'); - expect(r['intent'], 'classify-documents'); - expect(r['target'], 'home-server'); - expect(r['require_approval'], true); - expect(r['data_must_stay_local'], false); - }); - - test('rejects hallucinated enum values', () { - expect( - parseSetupSuggestion( - '{"scenario":"world-domination","intent":"hello-world",' - '"target":"this-laptop"}', - ), - isNull, - ); - }); - - test('rejects non-JSON replies', () { - expect(parseSetupSuggestion('I cannot help with that.'), isNull); - expect(parseSetupSuggestion('{broken json'), isNull); - }); - - test('missing booleans default to false', () { - final r = parseSetupSuggestion( - '{"scenario":"trying-out","intent":"hello-world",' - '"target":"this-laptop"}', - ); - expect(r, isNotNull); - expect(r!['require_approval'], false); - expect(r['data_must_stay_local'], false); - }); - }); } diff --git a/test/integration/hub_fixture.dart b/test/integration/hub_fixture.dart index 4ca24de..c376c0a 100644 --- a/test/integration/hub_fixture.dart +++ b/test/integration/hub_fixture.dart @@ -71,13 +71,13 @@ class HubFixture { if (binary == null) { if (skipIfBinaryMissing) { markTestSkipped( - 'Hub fixture skipped: no `chain` binary found via \$CHAIN_BIN, ' - 'PATH, or ../fai_chain/target/{release,debug}/chain. Build it ' - 'with `cargo build` in the platform repo and re-run.', + 'Hub fixture skipped: no `fai` binary found on PATH or in ' + '../chain_platform/target/release/fai. Build it with ' + '`cargo build --release` and re-run.', ); return null; } - throw StateError('chain binary not found'); + throw StateError('fai binary not found'); } final tempDir = await Directory.systemTemp.createTemp('chain_studio_test_'); @@ -157,36 +157,26 @@ class HubFixture { } } - /// Resolved hub binary path, for tests that also drive the CLI - /// against the fixture's data dir (e.g. seeding projects). - static Future binaryPath() => _resolveBinary(); - - /// Look for the hub binary: $CHAIN_BIN override first, then - /// `chain` on PATH (production-ish), then the release/debug - /// build outputs relative to the platform checkout (developer - /// flow, repo dir `fai_chain`). Returns null when nothing - /// resolves. + /// Look for `fai` first on PATH (production-ish), then in + /// the standard `target/release/` build output relative to + /// the platform checkout (developer flow). Returns null when + /// neither path resolves. static Future _resolveBinary() async { - final fromEnv = Platform.environment['CHAIN_BIN']; - if (fromEnv != null && fromEnv.isNotEmpty && File(fromEnv).existsSync()) { - return fromEnv; - } - final pathResult = await Process.run( Platform.isWindows ? 'where' : 'which', - [Platform.isWindows ? 'chain.exe' : 'chain'], + ['fai'], ); if (pathResult.exitCode == 0) { final s = (pathResult.stdout as String).trim(); if (s.isNotEmpty) return s.split('\n').first.trim(); } - final exe = Platform.isWindows ? 'chain.exe' : 'chain'; - for (final dir in ['release', 'debug']) { - final f = File('../fai_chain/target/$dir/$exe'); - if (await f.exists()) { - return f.absolute.path; - } + final candidate = Platform.isWindows + ? '../chain_platform/target/release/fai.exe' + : '../chain_platform/target/release/fai'; + final f = File(candidate); + if (await f.exists()) { + return f.absolute.path; } return null; } diff --git a/test/nav_manifest_test.dart b/test/nav_manifest_test.dart deleted file mode 100644 index 1646110..0000000 --- a/test/nav_manifest_test.dart +++ /dev/null @@ -1,118 +0,0 @@ -// Nav-manifest guard. The sidebar truth lives in three places in -// this repo: the `_pages` list (ids + order = Cmd numbers), the -// `labelOf` switch (id → l10n key), and the two .arb files (key → -// DE/EN label). The operator guide documenting that nav lives in -// the PLATFORM repo (fai_chain/docs/studio/operator-guide.md), -// which this repo's CI cannot see — so the truth is exported as a -// committed manifest, `docs/nav.generated.json`, and the platform -// repo's docs_consistency test checks the guide against it. -// -// This test derives the manifest from the sources. When it differs -// from the committed file it REWRITES the file and fails once with -// instructions — commit the regenerated manifest and mirror it to -// fai_chain/docs/studio/nav.generated.json so the cross-repo guard -// stays truthful. Add/rename/reorder a page and forget the guide: -// the platform gate goes loudly red instead of the docs quietly -// rotting. - -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; - -const _manifestPath = 'docs/nav.generated.json'; -const _mirrorHint = - 'fai_chain/docs/studio/nav.generated.json (same content)'; - -/// Extract the ordered page ids from the `_pages` list in main.dart. -List _pageIds(String mainDart) { - final block = RegExp( - r'static const _pages = <_NavPage>\[(.*?)\n \];', - dotAll: true, - ).firstMatch(mainDart); - if (block == null) { - fail('could not locate the _pages block in lib/main.dart'); - } - return RegExp(r"id: '([a-z-]+)'") - .allMatches(block.group(1)!) - .map((m) => m.group(1)!) - .toList(); -} - -/// Extract the id → l10n-key mapping from the `labelOf` switch. -Map _labelKeys(String mainDart) { - final keys = {}; - for (final m in RegExp( - r"case '([a-z-]+)':\s*\n\s*return l\.(nav\w+);", - ).allMatches(mainDart)) { - keys[m.group(1)!] = m.group(2)!; - } - if (keys.isEmpty) { - fail('could not parse the labelOf switch in lib/main.dart'); - } - return keys; -} - -Map _arb(String path) => - jsonDecode(File(path).readAsStringSync()) as Map; - -void main() { - test('docs/nav.generated.json matches the sidebar truth', () { - final mainDart = File('lib/main.dart').readAsStringSync(); - final ids = _pageIds(mainDart); - final labelKeys = _labelKeys(mainDart); - final de = _arb('lib/l10n/app_de.arb'); - final en = _arb('lib/l10n/app_en.arb'); - - final pages = >[]; - for (var i = 0; i < ids.length; i++) { - final key = labelKeys[ids[i]]; - expect( - key, - isNotNull, - reason: - "page '${ids[i]}' has no labelOf case — add it so the " - 'sidebar can localize the label', - ); - final labelDe = de[key] as String?; - final labelEn = en[key] as String?; - expect( - labelDe, - isNotNull, - reason: "l10n key '$key' missing from app_de.arb", - ); - expect( - labelEn, - isNotNull, - reason: "l10n key '$key' missing from app_en.arb", - ); - pages.add({ - 'id': ids[i], - 'cmd': i + 1, - 'label_de': labelDe, - 'label_en': labelEn, - }); - } - - const encoder = JsonEncoder.withIndent(' '); - final manifest = - '${encoder.convert({ - '_generated': 'by test/nav_manifest_test.dart — do not edit. ' - 'Mirror to $_mirrorHint.', - 'pages': pages, - })}\n'; - - final file = File(_manifestPath); - final current = file.existsSync() ? file.readAsStringSync() : null; - if (current != manifest) { - file.parent.createSync(recursive: true); - file.writeAsStringSync(manifest); - fail( - 'the sidebar nav changed: $_manifestPath was regenerated. ' - 'Commit it, mirror it to $_mirrorHint, and update the ' - 'operator guide — the platform repo\'s docs_consistency ' - 'test checks the guide against this manifest.', - ); - } - }); -}