Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app/Sources/BrokerDashboardStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,26 @@ final class BrokerDashboardStore {
&& serviceStatusUnverified == false
}

var canOfferReadOnlyFinishSetup: Bool {
var canOfferServiceRecoveryAction: Bool {
(startupState == .readOnlySnapshot || serviceRequiresRestart)
&& canStartBrokerService
&& isApplyingAction == false
&& serviceStatusUnverified == false
}

var guidedSetupActionTitle: String? {
switch startupState {
case .missingCLI, .serviceStatusUnverified, .ready:
return nil
case .needsHostBootstrap:
return "Complete first-time setup"
case .needsServiceStart, .readOnlySnapshot:
return serviceRequiresRestart ? "Finish setup" : "Start service"
case .needsSnapshotRefresh:
return "Finish setup"
}
}

var serviceAvailabilityMessage: String {
if serviceStatusUnverified {
if snapshot != nil {
Expand Down
4 changes: 2 additions & 2 deletions app/Sources/RootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,12 @@ struct RootView: View {
|| store.serviceRequiresRestart
{
VStack(alignment: .leading, spacing: 10) {
if store.canOfferReadOnlyFinishSetup {
if store.canOfferServiceRecoveryAction, let actionTitle = store.guidedSetupActionTitle {
StatusMessageCard(
color: .orange,
message: store.serviceAvailabilityMessage,
symbolName: "bolt.slash.fill",
actionTitle: "Finish setup",
actionTitle: actionTitle,
onAction: resumeGuidedSetup,
onDismiss: nil
)
Expand Down
15 changes: 1 addition & 14 deletions app/Sources/SharedViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ struct BrokerSetupView: View {
}

HStack(spacing: 12) {
if let primaryActionTitle = primaryActionTitle {
if let primaryActionTitle = store.guidedSetupActionTitle {
Button(primaryActionTitle, action: performPrimaryAction)
.buttonStyle(.borderedProminent)
.disabled(store.isApplyingAction || primaryActionEnabled == false)
Expand Down Expand Up @@ -554,19 +554,6 @@ struct BrokerSetupView: View {
}
}

private var primaryActionTitle: String? {
switch store.startupState {
case .missingCLI, .serviceStatusUnverified, .ready:
return nil
case .needsHostBootstrap:
return "Complete first-time setup"
case .needsServiceStart, .readOnlySnapshot:
return "Finish setup"
case .needsSnapshotRefresh:
return "Finish setup"
}
}

private func performPrimaryAction() {
store.requestGuidedSetup()
}
Expand Down
32 changes: 24 additions & 8 deletions app/Tests/BrokerDashboardStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ final class BrokerDashboardStoreTests: XCTestCase {
XCTAssertFalse(store.canSendCommands)
XCTAssertEqual(store.startupState, .serviceStatusUnverified)
XCTAssertTrue(store.serviceStatusUnverified)
XCTAssertFalse(store.canOfferReadOnlyFinishSetup)
XCTAssertFalse(store.canOfferServiceRecoveryAction)
XCTAssertEqual(store.serviceStatusText, "status unverified")
}

Expand Down Expand Up @@ -347,7 +347,7 @@ final class BrokerDashboardStoreTests: XCTestCase {
XCTAssertEqual(store.startupState, .serviceStatusUnverified)
XCTAssertEqual(store.lastErrorMessage, "Snapshot refresh failed once")
XCTAssertTrue(store.serviceStatusUnverified)
XCTAssertFalse(store.canOfferReadOnlyFinishSetup)
XCTAssertFalse(store.canOfferServiceRecoveryAction)
XCTAssertEqual(store.serviceStatusText, "status unverified")
XCTAssertEqual(
store.serviceAvailabilityMessage,
Expand Down Expand Up @@ -403,7 +403,7 @@ final class BrokerDashboardStoreTests: XCTestCase {
XCTAssertEqual(store.commandStatusText, "Status unverified")
XCTAssertEqual(store.serviceStatusText, "status unverified")
XCTAssertFalse(store.canStartBrokerService)
XCTAssertFalse(store.canOfferReadOnlyFinishSetup)
XCTAssertFalse(store.canOfferServiceRecoveryAction)
XCTAssertEqual(
store.serviceAvailabilityMessage,
"Broker commands are disabled because current brokerd status could not be verified. Refresh before starting setup or sending commands."
Expand Down Expand Up @@ -597,7 +597,7 @@ final class BrokerDashboardStoreTests: XCTestCase {
XCTAssertFalse(store.serviceStatusUnverified)
XCTAssertEqual(store.startupState, .readOnlySnapshot)
XCTAssertTrue(store.canStartBrokerService)
XCTAssertTrue(store.canOfferReadOnlyFinishSetup)
XCTAssertTrue(store.canOfferServiceRecoveryAction)
}

func testUnreadableSnapshotWithValidatedLiveServiceRemainsUnverified() async throws {
Expand Down Expand Up @@ -655,7 +655,8 @@ final class BrokerDashboardStoreTests: XCTestCase {
XCTAssertFalse(store.serviceStatusUnverified)
XCTAssertFalse(store.canSendCommands)
XCTAssertTrue(store.canStartBrokerService)
XCTAssertTrue(store.canOfferReadOnlyFinishSetup)
XCTAssertTrue(store.canOfferServiceRecoveryAction)
XCTAssertEqual(store.guidedSetupActionTitle, "Finish setup")
XCTAssertEqual(store.startupState, .needsServiceStart)
XCTAssertEqual(store.commandStatusText, "Restart required")
XCTAssertEqual(store.serviceStatusText, "brokerd restart required")
Expand Down Expand Up @@ -781,7 +782,7 @@ final class BrokerDashboardStoreTests: XCTestCase {
XCTAssertEqual(store.startupState, .serviceStatusUnverified)
XCTAssertFalse(store.canSendCommands)
XCTAssertFalse(store.canStartBrokerService)
XCTAssertFalse(store.canOfferReadOnlyFinishSetup)
XCTAssertFalse(store.canOfferServiceRecoveryAction)
}

func testRefreshFailureDismissesGuidedSetupConfirmationAndBlocksLocalApply() async throws {
Expand Down Expand Up @@ -1327,10 +1328,24 @@ final class BrokerDashboardStoreTests: XCTestCase {
"Broker commands are disabled because brokerd is not running. Start the service to enable pinning, release, and lifecycle actions."
)
XCTAssertTrue(store.canStartBrokerService)
XCTAssertTrue(store.canOfferReadOnlyFinishSetup)
XCTAssertTrue(store.canOfferServiceRecoveryAction)
XCTAssertEqual(store.guidedSetupActionTitle, "Start service")

store.isApplyingAction = true
XCTAssertFalse(store.canOfferReadOnlyFinishSetup)
XCTAssertFalse(store.canOfferServiceRecoveryAction)
}

func testConfiguredStoppedServiceActionStartsService() {
let loadedState = makeLoadedState(snapshot: nil, service: nil)
let store = BrokerDashboardStore(
loader: StubSnapshotLoader(state: loadedState),
commandClient: RecordingCommandClient(),
runtimePaths: loadedState.paths
)
store.loadedState = loadedState

XCTAssertEqual(store.startupState, .needsServiceStart)
XCTAssertEqual(store.guidedSetupActionTitle, "Start service")
}

func testStartupStatePrioritizesMissingHostBootstrapWhenServiceExists() {
Expand Down Expand Up @@ -1364,6 +1379,7 @@ final class BrokerDashboardStoreTests: XCTestCase {
store.loadedState = loadedState

XCTAssertEqual(store.startupState, .needsHostBootstrap)
XCTAssertEqual(store.guidedSetupActionTitle, "Complete first-time setup")
}

func testGuidedSetupPreviewsThenAppliesTheExactPlan() async throws {
Expand Down
13 changes: 9 additions & 4 deletions spec/tasks/guided-simbroker-setup.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Guided `simbroker setup`

> **Document ID:** `GSB-SETUP-001`
> **Version:** `1.0.33`
> **Last Updated:** `2026-08-30`
> **Version:** `1.0.34`
> **Last Updated:** `2026-09-01`
> **Status:** `Active`
> **Owner:** `spec-steward`, `ios-dev`
> **Target:** Shipped on `v0.1.0-alpha.3`
Expand Down Expand Up @@ -420,8 +420,12 @@ is already `ready` refreshes the current snapshot before reporting that
success, matching confirmed apply. Failure of a confirmable plan refreshes
first, preserves the sheet, and shows completed stages and recovery.
Automatic finishing failure stays off the sheet and surfaces the recovery
error on the dashboard. The read-only snapshot `Finish setup` action is hidden
while setup is applying so an in-progress run can only be stopped through Stop.
error on the dashboard. A configured host with stopped `brokerd` labels its
recovery action `Start service`; an incompatible live runtime keeps the
`Finish setup` action, and missing host configuration keeps `Complete
first-time setup`. The
read-only recovery action is hidden while setup is applying so an in-progress
run can only be stopped through Stop.
Displayed setup failures include the CLI `logPath`, `doctorIssues` including
per-issue `remediationCommands` when present, and incomplete rollback count
when those fields are present.
Expand Down Expand Up @@ -625,6 +629,7 @@ long-running plan/handoff/evaluation, and a passing `agent:complete`.

| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0.34 | 2026-09-01 | `spec-steward`, `ios-dev` | Give configured stopped-service recovery an explicit `Start service` action while preserving `Finish setup` for runtime restart and first-time setup copy for missing configuration |
| 1.0.33 | 2026-08-30 | `spec-steward`, `ios-dev` | Make app live-service reads and mutations require the exact generated broker runtime version |
| 1.0.32 | 2026-08-30 | `spec-steward`, `ios-dev` | Treat a live unhealthy `brokerd` as setup finishing work with service action `start`, and honor first SIGINT/SIGTERM while replacing that daemon |
| 1.0.31 | 2026-08-29 | `spec-steward`, `ios-dev` | Record Target as shipped on `v0.1.0-alpha.3` so the spec matches the tagged Homebrew, npm, and notarized-app artifacts that include guided setup |
Expand Down