A second product, not a second edition: dev.rzen.indie.KanbanMobile (iOS 26,
iPhone-only) compiles Kanban/Storage as source files, so a format change that
breaks the phone breaks this build the day it's made. Boards live in
iCloud.dev.rzen.indie.Kanban — named after the Mac bundle id so the Mac app can
adopt the container later without a migration; until then the folder is
"Lanework" in iCloud Drive and the Mac opens boards there through the open
panel.
EchoLedger grows #if os(macOS) gates around its three consumer surfaces
(verdicts/BoardDiff, harvest/HarvestedReceipt, comment retirement/CommentPath)
— the recording side BoardWriter stamps compiles on every platform, and the
gates are the seam a future phone verdict surface lands behind. AgentGuide
stays Mac-only.
The phone's watcher is NSMetadataQuery: BoardIndexStore (one query, package
UTI export makes a .kanban directory one item, equality-gated rescans,
download kicks per pass), BoardSession (materialization sweep before every
fail-fast walk, NSFileCoordinator brackets, perform{} = coordinated write then
awaited reload, ParseMemo threaded), CloudHome (off-main container resolution,
LANEWORK_LOCAL_ROOT DEBUG override for simulator work without an account).
Screens: Boards -> lanes -> cards -> card editor, value-routed by ItemID with
every screen re-reading the live snapshot; leading swipe moves a card via
confirmationDialog, trailing swipe sends it to .trash/; the editor commits
title through the Mac's canonical rename path and body through writeBody, with
drafts that survive reloads; attributes are the three typed style fields
(icon, iconColor, background) — labels is a reserved unknown-field key and
deliberately has no editor. Settings carries IndieBackup (backup root = the
container's Documents, restore rebuild = an index rescan, controller
constructed only once the home resolves).
Arbiter: KanbanMobile green for iOS Simulator, Kanban green for macOS, 3006
unit tests / 517 suites passed (PointerLatencyTests excluded — mid-rework
uncommitted in a parallel session).
Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
69 lines
3.5 KiB
Swift
69 lines
3.5 KiB
Swift
import Foundation
|
|
import IndieBackup
|
|
import SwiftUI
|
|
|
|
/// IndieBackup's app-specific wiring (indie-backup skill): what gets backed up, and how the
|
|
/// app's in-memory board list is rebuilt after a restore.
|
|
///
|
|
/// **Why this type is constructed rather than defaulted.** `BackupConfiguration.backupRoot` is a
|
|
/// non-optional `URL`, but the ubiquity container resolves asynchronously and can fail outright
|
|
/// (`CloudHomeResolver`, CloudHome.swift) — there is no root to hand IndieBackup until
|
|
/// `BoardIndexStore.home` exists. Rather than answer with a placeholder path, this type is only
|
|
/// ever constructed once a real `CloudHome` is in hand (`MobileApp`'s `.task(id:)`); before that
|
|
/// there is no `BackupController` at all, and `SettingsTabView` renders a quiet disabled
|
|
/// explanation instead of a section built on a fabricated root.
|
|
final class MobileBackupConfiguration: BackupConfiguration {
|
|
/// No dot. Shared by the Info.plist document-type/UTI registration
|
|
/// (`dev.rzen.indie.kanban-backup`) and the archive filenames IndieBackup writes.
|
|
static let fileExtension = "kanbanbackup"
|
|
|
|
private let root: URL
|
|
private let index: BoardIndexStore
|
|
|
|
/// - Parameters:
|
|
/// - root: The resolved `CloudHome.documentsURL` — the same `Documents/` folder every
|
|
/// `.kanban` board lives directly inside. IndieBackup walks it file-by-file (it has no
|
|
/// notion of "package"), so a board's directory structure round-trips through a backup
|
|
/// as an ordinary tree of files — no directory registration or special-casing needed.
|
|
/// - index: Rescanned after a restore replaces the tree, in `rebuildCacheAfterRestore`.
|
|
init(root: URL, index: BoardIndexStore) {
|
|
self.root = root
|
|
self.index = index
|
|
}
|
|
|
|
var backupFileExtension: String { Self.fileExtension }
|
|
var backupDisplayName: String { "Lanework Backup" }
|
|
var backupRoot: URL { root }
|
|
|
|
/// `BoardIndexStore` caches nothing durable — no Core Data, no SwiftData, no file on disk —
|
|
/// only an in-memory `[BoardSummary]` scanned straight from the files under `backupRoot`. A
|
|
/// restore already replaced that tree by the time this runs, so the only rebuild step is
|
|
/// asking the store to rescan it; a `BoardSession` for a board that's open elsewhere reloads
|
|
/// its own content from disk independently the next time it's asked (BoardIndexStore's doc
|
|
/// comment on the observer flow).
|
|
///
|
|
/// `refresh()` only *starts* the rescan (it hands off to a detached `Task` and returns), so
|
|
/// this reports done as soon as the request lands, not once the boards tab has repainted —
|
|
/// same as any other call to `refresh()` in this app (e.g. pull-to-refresh).
|
|
func rebuildCacheAfterRestore(progress: @escaping (Double, String) -> Void) async throws {
|
|
progress(0, "Rescanning boards…")
|
|
await index.refresh()
|
|
progress(1, "Rescanning boards…")
|
|
}
|
|
}
|
|
|
|
/// Threads the lazily-constructed `BackupController` from `MobileApp` down to `SettingsTabView`
|
|
/// (the backup section) and back up to `MobileApp`'s own `onOpenURL` handler (imported backup
|
|
/// files), without forcing `RootTabView` — or anything else between them — to know backup exists.
|
|
/// `nil` until the cloud home resolves; see `MobileBackupConfiguration`.
|
|
private struct BackupControllerKey: EnvironmentKey {
|
|
static let defaultValue: BackupController? = nil
|
|
}
|
|
|
|
extension EnvironmentValues {
|
|
var backupController: BackupController? {
|
|
get { self[BackupControllerKey.self] }
|
|
set { self[BackupControllerKey.self] = newValue }
|
|
}
|
|
}
|