Files
lanework/Kanban/App/RestoreBootstrapView.swift
T
rzen 31fee00c73 The decision surface — a refused open becomes a live repair, in place
Phase 3 of the decision surface, completing the card (01 ▸ Malformed
input, settled 2026-07-31). An attended open's fail-fast walk transforms
the loading window's content into one aggregated surface — never a
sheet, never a chain: defects grouped by class, each class stated once
with its files listed (Reveal in Finder + Open in Editor per row), a
class-level default preselected, per-item override behind a disclosure.
Only honest choices: YAML and malformed-schema get Editor + Re-check
(Skip below the root); newer-than-app gets Skip alone and blocks the
board at the root; the two root repairs — minted index, schema: 1 stamp
— are defaults. Repair and Open applies fixes in one store-less write
bracket and re-walks: clean proceeds, remainder re-aggregates into the
same surface. Cancel and ⌘W retire to welcome's row; restored opens
never see the surface at all (OpenOrigin rides the PendingOpen carrier).

Skips are per-open consent that rides the session — the store retains
the skip set and every reload passes it — and the opened board posts a
warning-tone notice naming what was left out, each item's Reveal riding
the banner strip's new reveal control. On Pro boards the repair bracket
binds its own EchoLedger, heal-marks everything, and the store adopts it
before the committer starts, so repairs land as one separate commit
authored Lanework Integrity — pinned end to end. Also fixed en route: a
retired loading window left its close interception installed and
returned false from windowShouldClose forever, blocking quit.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-08-01 10:52:02 -04:00

141 lines
7.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import os
/// The launch-time restoration pass, wearing a window because that is the only place SwiftUI lets
/// work like this run.
///
/// ### Why a window at all
///
/// Restoration has to open windows, and opening a window needs `openWindow`, which is only readable
/// from a view. An `App.init()` cannot do it and `AppDelegate` has no environment. So the app
/// presents one throwaway window at launch — 1×1, plain, ordered straight back out, absent from the
/// Window menu — whose only job is to run the pass and then dismiss itself. It exists for a few
/// hundred milliseconds and never draws.
///
/// It is presented at **every** launch — it is the app's one reliable presenter (see `KanbanApp`'s
/// bootstrap scene for the macOS 26 behavior that forced this), so even the plain launch-to-welcome
/// path runs through it: the pass finds nothing flagged and opens welcome itself.
///
/// ### What the pass does
///
/// Reads the registry's flagged records in `lastOpened` order (`BoardRegistry.restorables()`), opens
/// the available ones, and records the unavailable ones as failures — 02 § Launch and window
/// lifecycle: "Other restorations proceed unaffected — never a launch-time modal chain, never a
/// silent drop." Welcome comes up only if nothing was even attempted; a board that *was* attempted
/// and then failed to load opens welcome from its own host, which is the same rule applied one layer
/// down and keeps this pass from having to wait on loads it did not perform.
///
/// ### And one other pass, for the same reason
///
/// The accessibility audit suite's fixture board (`UITestLaunch`) is built and opened here too. It is
/// the same job with a different source — filesystem work that must happen before the first real
/// window, needing `openWindow` to finish — and giving it a second throwaway window would be a second
/// copy of everything this file explains. Which pass runs is `plan`'s to say and nothing else's.
struct RestoreBootstrapView: View {
/// Decided in `KanbanApp.init()`; this view only dispatches on it.
let plan: LaunchPlan
@Environment(AppModel.self) private var appModel
@Environment(\.openWindow) private var openWindow
@Environment(\.dismissWindow) private var dismissWindow
@State private var windowController = HostedWindowController()
private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "launch")
var body: some View {
Color.clear
.frame(width: 1, height: 1)
.background(WindowAccessor(controller: windowController))
.onAppear {
// Out of sight before it can be seen. `orderOut` rather than a hidden style because
// the scene must still exist — a window SwiftUI never presents never runs its task.
windowController.onAttach = { window in
window.alphaValue = 0
window.orderOut(nil)
}
if let window = windowController.window {
windowController.onAttach?(window)
}
}
.task { await restore() }
}
private func restore() async {
// Captured directly rather than waiting for `CaptureOpenWindow`'s `onAppear`: this task is
// the app's first act, and `openBoard` needs the action now. The count is a cold Finder-open
// that arrived before this window did — a board already on its way to the screen, which the
// pass below must count as an open or it would put welcome up beside the user's document.
let replayedOpens = appModel.captureWindowActions(open: openWindow, dismiss: dismissWindow)
switch plan {
case .uiTestFixture:
openFixtureBoard()
case .restoreBoards, .welcome:
// `.welcome` arrives here by design — this window presents at every launch, because it is
// the app's one reliable presenter (see `KanbanApp`'s bootstrap scene) — and the pass is
// its answer: nothing is flagged, so it shows welcome, which is what `.welcome` asked
// for.
restoreFlaggedBoards(openedAlready: replayedOpens)
}
dismissWindow(id: WindowID.restoreBootstrap)
}
private func restoreFlaggedBoards(openedAlready: Int) {
var attempted = openedAlready
for board in appModel.boardRegistry.restorables() {
switch board {
case let .available(_, url):
// **The one restored open in the app** (01-storage-format.md § Malformed input, the
// decision surface): a board that fails here keeps today's retire-to-welcome-row
// landing — "launch never chains dialogs", and nobody is sitting in front of a
// restoration waiting to repair four boards at once. The row's retry click is the
// attended open that then shows the surface.
appModel.openBoard(at: url, origin: .restored)
attempted += 1
case let .unavailable(record):
Self.logger.error("a flagged board could not be restored — its bookmark no longer resolves")
appModel.recordLaunchFailure(
path: record.lastKnownPath,
message: "This board is unavailable. Its volume may be offline, or it may have been moved or deleted."
)
}
}
if attempted == 0 {
appModel.showWelcome()
}
}
/// The UI suites' board: built here, opened through the same `openBoard` every other path uses,
/// so it registers, bookmarks and titles itself exactly like a board the user opened.
///
/// **Which board is the launch arguments' to say** (`UITestLaunch.variant`), and this method does
/// not care: the malformed variant is built and opened exactly like the other two, and its
/// failure arrives one layer down as the *loader's*. It opens **attended**, like every other
/// board a person asks for, so its refusal transforms the loading window into the decision surface
/// (`BoardWindowHost.handleWalkFailure`) rather than retiring — which is precisely what the
/// fail-fast UI pass is there to see. Special-casing it here would replace the behaviour under
/// test with a behaviour about the fixture.
///
/// **A failure to *build* lands on welcome as an ordinary launch failure**, with the fixture's own
/// path on it. That is deliberate: a suite whose fixture failed to build would otherwise audit an
/// empty screen and pass, which is the one outcome an accessibility gate must never produce.
private func openFixtureBoard() {
let variant = UITestLaunch.variant
do {
let url = try UITestLaunch.materializeFixtureBoard(variant)
appModel.openBoard(at: url)
} catch {
Self.logger.error("the UI-test fixture board could not be built: \(error.localizedDescription, privacy: .public)")
appModel.recordLaunchFailure(
path: UITestLaunch.fixtureBoardURL(for: variant).path,
message: "The UI-test fixture board could not be built: \(error.localizedDescription)"
)
appModel.showWelcome()
}
}
}