Replace ZStack-based cell with Color.clear.aspectRatio(1,.fit) + .overlay.
The overlay constrains all children (image, video badge, sync dot) to the
exact square frame, so scaledToFill cannot inflate the cell height in
LazyVGrid. Previously, the ZStack let the filled image push beyond the
aspectRatio constraint, causing inconsistent row heights.
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
Phase 1 was only checking the Kisani manifest for sync status. Phone photos
uploaded by other tools had no manifest entry, showed as orange (phone-only)
even though they were already on the NAS.
Now Phase 1 falls back to a filename lookup against the full NAS directory
listing: manifest match → green (.synced), NAS directory match → green (.synced),
no match → orange (.phone). Phase 3 skips filenames already resolved in Phase 1.
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
Gallery NAS was only showing Kisani-manifest-tracked files (2 of 7432). Now
fetches the full NAS directory listing on load and adds Phase 3 items for files
not in the manifest (uploaded by other tools) — all 7432 files appear in the
gallery with thumbnails loaded lazily on scroll.
Settings network section: "No Wi-Fi" → "Wi-Fi" when connected but SSID is nil
(no location permission). Added cellular status row below Wi-Fi row.
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
GalleryViewModel.load() now has a three-tier resolution for manifest entries:
1. statusService.lastManifest — in-memory, freshest
2. NASManifestCache disk cache — survives restarts
3. Direct NAS download — fallback when both caches are cold (first launch after install,
or before BackupStatusService reconcile has completed)
The direct download also populates NASManifestCache so subsequent opens are instant.
Also adds GalleryViewModel.bind(to:connection:) which subscribes to
statusService.$lastManifest and automatically reloads gallery items when
BackupStatusService finishes reconciling — gallery updates without manual pull-to-refresh.
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
Cached indexes:
- NASManifestCache: actor-based disk cache for the NAS manifest (kisani_manifest_cache.json)
Avoids NAS network round-trip when cache < 5 min old. Updated after every manifest download.
- LocalPhotoIndex: actor-based disk cache of phone asset metadata (kisani_local_index.json)
Built once on first launch, then updated incrementally via PHPhotoLibraryChangeObserver.
O(1) countSafe via in-memory dict (vs PHFetchResult batch enumeration + CoreData calls).
Fast path reconcile:
When both caches are warm, reconcile() skips NAS connection + PHFetchResult enumeration
entirely and returns in microseconds. Falls through to full NAS path only when stale.
AutoBackupCoordinator.onActive():
Changed force: true → force: false so the 30-second debounce prevents expensive rescans
on rapid app foreground/background cycles. Dashboard appears instantly from cached snapshot.
BackupQueueItem + queue tracking:
BackupEngine publishes queueItems: [BackupQueueItem] with per-file status, progress,
speed, error details. Queue is persisted to disk and reloaded on next launch so SyncView
always has data to show even after restart.
NotificationService:
Centralised local notification sender replacing the inline UNMutableNotificationContent
blocks. Sends for: backup started, completed, failed, paused, NAS offline. Uses category
identifiers so same-type notifications replace each other.
SyncView redesign:
- Removed pill/chip around NAS IP — replaced with plain "Online/Offline/Checking" dot+text
- No live NAS directory listing (was expensive NAS connection on every tab open)
- Shows engine.queueItems grouped into Active / Failed / Completed sections
- Per-file rows with inline progress bar + bytes/speed for uploading items, error text for failed
- Idle state shows archive count from statusService snapshot
GalleryViewModel:
NAS tab and All tab now use NASManifestCache as fallback when statusService.lastManifest
is nil (offline launch). Gallery shows NAS items even when NAS is unreachable.
AppDelegate:
Bootstraps LocalPhotoIndex.loadOrBuild() in background on launch (non-blocking).
Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
- Rename app to Kisani in all UI text and Face ID prompt
- Replace flat 2-state RootView with 5-step flow: ConnectView →
LoginView → FolderSetupView → PhotoPermissionView → MainTabView
- Remove NavigationStack from LoginView (was causing nested-stack crash);
drive navigation via ConnectionStore.isSessionActive
- Add FolderSetupView and PhotoPermissionView onboarding screens with
step indicator (2/3, 3/3), spring entrance animations, and UGreen
connection chip
- Fix dark mode button invisibility: PrimaryButtonStyle and PillButtonStyle
now use AppTheme.inkInverse; GhostButtonStyle border uses ink.opacity(0.25)
- Add AppTheme.inkInverse adaptive token (dark bg ink on dark, white on light)
- Add ConnectionStore.isSessionActive (non-persisted) and
onboardingPhotoShown (UserDefaults-persisted)
- Add os_log logging in LoginViewModel at auth start/success/failure
- Remove dead Help button from ConnectView
- UGreenCompatibilityBadge, KeychainStore, SavedConnections, extensions,
PrivacyInfo.xcprivacy, unit test target and test stubs
- AppTheme: full UIColor dynamic provider tokens for dark/light adaptive color
- AppearanceMode enum + segmented picker in SettingsView
- BackupView: enlarged ring, removed options card and linear progress bar
- HistoryView: duration and bytes formatting, improved typography hierarchy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>