The phone joins the format — KanbanMobile MVP: shared storage verbatim over an iCloud container
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
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import SwiftUI
|
||||
import IndieBackup
|
||||
import os
|
||||
|
||||
/// The iPhone app's entry point. Two tabs, per the mobile MVP charter: Boards (the whole
|
||||
/// board → lane → card navigation stack) and Settings (backup/restore).
|
||||
@main
|
||||
struct MobileApp: App {
|
||||
|
||||
/// **The app's root model, owned here and nowhere else** — the container home, the one
|
||||
/// `NSMetadataQuery` the process runs, and the per-board session cache. `@State` because the
|
||||
/// scene owns its lifetime; every screen reaches it through `.environment`, never by
|
||||
/// constructing one.
|
||||
@State private var boardIndex = BoardIndexStore()
|
||||
|
||||
/// Backup/restore (indie-backup skill), built once `boardIndex.home` resolves to a real
|
||||
/// `CloudHome` — see `MobileBackupConfiguration`'s doc comment for why this can't happen at
|
||||
/// `@State` init time. Stays `nil` for the lifetime of a run with no iCloud account;
|
||||
/// `SettingsTabView` renders the quiet disabled state for exactly that case.
|
||||
@State private var backupController: BackupController?
|
||||
|
||||
private static let logger = Logger(subsystem: "dev.rzen.indie.KanbanMobile", category: "backup")
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootTabView()
|
||||
.environment(boardIndex)
|
||||
.environment(\.backupController, backupController)
|
||||
.task(id: boardIndex.home) {
|
||||
guard backupController == nil, let home = boardIndex.home else { return }
|
||||
backupController = BackupController(
|
||||
configuration: MobileBackupConfiguration(root: home.documentsURL, index: boardIndex)
|
||||
)
|
||||
}
|
||||
.onOpenURL { url in
|
||||
// A backup file opened from Files, AirDrop, or a share sheet. Silently
|
||||
// ignored before `backupController` exists — there is no resolved cloud home
|
||||
// to restore into either, at that point.
|
||||
guard url.pathExtension == MobileBackupConfiguration.fileExtension,
|
||||
let backupController
|
||||
else { return }
|
||||
Task {
|
||||
do {
|
||||
try await backupController.restoreBackup(from: url)
|
||||
} catch {
|
||||
Self.logger.error("restore from opened file failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RootTabView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
Tab("Boards", systemImage: "rectangle.stack") {
|
||||
BoardsTabView()
|
||||
}
|
||||
Tab("Settings", systemImage: "gearshape") {
|
||||
SettingsTabView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user