Home app-side state in the shared App Group container
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
This commit is contained in:
@@ -244,12 +244,13 @@ struct WelcomeView: View {
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
/// Opens a row's board. Welcome closes itself on the way in — that is the board window host's
|
||||
/// job ("Opening a board from welcome closes welcome"), not this view's, because the close has to
|
||||
/// wait for the load to actually succeed.
|
||||
/// Opens a row's board — through `AppModel.open(_:)`, which owns the two ways a row can lead to
|
||||
/// one (an available URL, or the re-grant panel a cross-edition row needs first). Welcome closes
|
||||
/// itself on the way in — that is the board window host's job ("Opening a board from welcome
|
||||
/// closes welcome"), not this view's, because the close has to wait for the load to actually
|
||||
/// succeed.
|
||||
private func open(_ row: WelcomeRow) {
|
||||
guard let url = row.url else { return }
|
||||
appModel.openBoard(at: url)
|
||||
appModel.open(row)
|
||||
}
|
||||
|
||||
private func reveal(_ row: WelcomeRow) {
|
||||
@@ -320,8 +321,9 @@ private struct RecentBoardRow: View {
|
||||
}
|
||||
.padding(.vertical, BoardMetrics.em(0.3, bodyPointSize: WelcomeView.pointSize))
|
||||
// Dimmed when the board cannot be reached — the row stays, with Forget, rather than
|
||||
// disappearing (02 § Graceful orphaning).
|
||||
.opacity(row.isAvailable ? 1 : 0.55)
|
||||
// disappearing (02 § Graceful orphaning). A cross-edition row is *not* dimmed: it opens on one
|
||||
// click like any other, and dimming it would advertise a loss that has not happened.
|
||||
.opacity(row.canOpen ? 1 : 0.55)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
@@ -363,6 +365,14 @@ private struct RecentBoardRow: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
case .needsReopen:
|
||||
// Not a warning tone: nothing is wrong and nothing is lost — this board came from the
|
||||
// other edition's list and needs one grant (12-editions.md ▸ Distribution). The words say
|
||||
// what the click will do, since the click is the whole remedy.
|
||||
Label("Open once to grant access", systemImage: "hand.raised")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
case let .failed(message):
|
||||
// The warning tint, and the whole of fail-fast's specifics — this row *is* the failure
|
||||
// surface (02 § Launch and window lifecycle).
|
||||
@@ -385,7 +395,10 @@ private struct RecentBoardRow: View {
|
||||
/// turned off and then turns it back on.
|
||||
struct SettingsView: View {
|
||||
|
||||
@AppStorage(AppPreferences.restoreOpenBoardsAtLaunchKey)
|
||||
/// `store:` named explicitly, and it has to be: the key lives in the group's shared suite
|
||||
/// (`AppPreferences`), and `@AppStorage`'s default domain is `.standard` — a different one. A
|
||||
/// toggle bound to the wrong domain would write a preference the launch flow never reads.
|
||||
@AppStorage(AppPreferences.restoreOpenBoardsAtLaunchKey, store: AppGroup.defaults)
|
||||
private var restoreOpenBoardsAtLaunch = true
|
||||
|
||||
var body: some View {
|
||||
|
||||
Reference in New Issue
Block a user