Every edition declares group.dev.rzen.indie.Kanban and homes its
app-side state there from day one (12-editions.md ruling 2026-07-29):
- AppGroup namespace: container resolution with per-edition fallback
when unprovisioned, shared UserDefaults suite, edition identity, and
a unit-test-host redirect (the test host IS the app — its launch
sweep and recents refresh must not touch the real shared container).
- BoardRecord: bookmark/isOpenNow replaced by per-edition grants and
openNow keyed by bundle id; hand-written Codable keeps legacy keys
decoding (adopted in memory as the running edition's slots, upgraded
on first save); every other field stays common.
- RecentBoard gains needsReopen: no grant of ours but somebody's —
first click runs an open panel pre-anchored at the recorded path,
prompt "Grant"; recordOpen mints this edition's slot onto the
matched shared record (path fallback only after identity fails and
only against records holding no grant of ours, so re-granting never
forks the record).
- Cross-edition freshness: stat-cheap mtime+size stamp re-reads the
registry when the sibling edition wrote it, so one edition's save
never erases the other's records wholesale.
- restorables() filters on this edition's open-now flags; the board
popover gains BoardEditionPresence ("Also open in Lanework Pro"),
pid-liveness-checked so crash residue never lies.
- Clipboard staging store moves to the group container; the sweep
claims doomed trees by atomic rename into .sweeping/ then deletes,
so the sibling's concurrent sweep is a non-event.
- Template store re-homed to the group container per the 09-templates
re-ruling; scalars (quick-style recents, window size) move to the
shared suite.
- verify-editions.sh: 30 checks (each edition carries exactly the
family group). No pathfinder 1.x migrator: 1.x predates the
registry; state starts fresh in the group container.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
141 lines
7.0 KiB
Swift
141 lines
7.0 KiB
Swift
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 **only** when there is something to open (`KanbanApp` decides, via
|
||
/// `LaunchPlan.presentsBootstrap`), so the ordinary launch-to-welcome path never creates it.
|
||
///
|
||
/// ### 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.
|
||
appModel.captureWindowActions(open: openWindow, dismiss: dismissWindow)
|
||
|
||
switch plan {
|
||
case .uiTestFixture:
|
||
openFixtureBoard()
|
||
case .restoreBoards, .welcome:
|
||
// `.welcome` never presents this window, so it cannot arrive here — and if a future
|
||
// launch path let it, the restoration pass is the harmless answer: it finds nothing
|
||
// flagged and shows welcome, which is what `.welcome` asked for anyway.
|
||
restoreFlaggedBoards()
|
||
}
|
||
|
||
dismissWindow(id: WindowID.restoreBootstrap)
|
||
}
|
||
|
||
private func restoreFlaggedBoards() {
|
||
var attempted = 0
|
||
for board in appModel.boardRegistry.restorables() {
|
||
switch board {
|
||
case let .available(_, url):
|
||
appModel.openBoard(at: url)
|
||
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."
|
||
)
|
||
case .needsReopen:
|
||
// Effectively unreachable — this edition can only have flagged a board open by having
|
||
// opened it, which needed a grant — and deliberately quiet if it ever happens.
|
||
//
|
||
// **No launch failure and no panel.** A modal grant panel at launch is exactly the
|
||
// "launch-time modal chain" 02 § Launch and window lifecycle rules out, and a failure
|
||
// row would put fail-fast's warning tone over a board that is *fine*: welcome appears
|
||
// (nothing restored), and this board's own row already carries the re-grant caption
|
||
// and the one click that resolves it (12-editions.md ▸ Distribution). That row is the
|
||
// surface, so nothing is silently dropped.
|
||
Self.logger.error("a flagged board is awaiting this edition's grant; left for its welcome row")
|
||
}
|
||
}
|
||
|
||
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* — a board window that records fail-fast's own
|
||
/// sentence and dismisses itself (`BoardWindowHost.start`). Special-casing it here would replace
|
||
/// the sentence under test with a sentence 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()
|
||
}
|
||
}
|
||
}
|