Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
import Foundation
|
|
|
|
|
import Network
|
|
|
|
|
import NetworkExtension
|
Add cellular data toggle, Tailscale remote access, fix ToggleRow padding
- ToggleRow: fix cramped layout — proper horizontal + vertical card padding
- ConnectionStore: add allowCellularBackup, useTailscaleWhenRemote, tailscaleHost
- LANMonitor: add isOnCellular (NWPath cellular interface), isTailscaleActive
(detects utun interface with 100.x.x.x CGNAT address), openTailscaleApp()
- BackupEngine: gate backup on cellular setting; resolveHost() switches to
tailscaleHost when not on trusted LAN and Tailscale tunnel is active
- SettingsView: CONNECTIVITY section (cellular toggle + live status),
REMOTE ACCESS section (Tailscale toggle, host field, status dot, Open button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:40:07 +03:00
|
|
|
import Darwin
|
|
|
|
|
import UIKit
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
import Combine
|
|
|
|
|
|
|
|
|
|
@MainActor
|
|
|
|
|
final class LANMonitor: ObservableObject {
|
|
|
|
|
static let shared = LANMonitor()
|
|
|
|
|
|
|
|
|
|
@Published private(set) var isOnNetwork: Bool = false
|
|
|
|
|
@Published private(set) var currentSSID: String? = nil
|
Add cellular data toggle, Tailscale remote access, fix ToggleRow padding
- ToggleRow: fix cramped layout — proper horizontal + vertical card padding
- ConnectionStore: add allowCellularBackup, useTailscaleWhenRemote, tailscaleHost
- LANMonitor: add isOnCellular (NWPath cellular interface), isTailscaleActive
(detects utun interface with 100.x.x.x CGNAT address), openTailscaleApp()
- BackupEngine: gate backup on cellular setting; resolveHost() switches to
tailscaleHost when not on trusted LAN and Tailscale tunnel is active
- SettingsView: CONNECTIVITY section (cellular toggle + live status),
REMOTE ACCESS section (Tailscale toggle, host field, status dot, Open button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:40:07 +03:00
|
|
|
@Published private(set) var isOnCellular: Bool = false
|
|
|
|
|
@Published private(set) var isTailscaleActive: Bool = false
|
feat: Sync tab, hamburger menu, network status, browse UX
- Replace History tab with Sync tab (file list, NAS status, free-up-space retention)
- Add AppMenuView hamburger sheet (History, Errors, Connection Status, Sync Activity)
- BackupView: Kisani logo chip in toolbar, hamburger menu button, swipeable ring
pages (progress/pending/failed/uploaded), always-visible compact stats strip
- SettingsView: NETWORK section (SSID, NAS reachability, trusted-network warning,
refresh button)
- BrowseView: pull-to-refresh, long-press context menu on items
- LANMonitor: nasReachable, nasCheckTime, checkNASReachability(host:port:)
- ConnectionStore: localRetentionDays (0/60/90/365 days)
- Regenerate xcodeproj with xcodegen to include new files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 17:58:00 +03:00
|
|
|
@Published private(set) var nasReachable: Bool? = nil
|
|
|
|
|
@Published private(set) var nasCheckTime: Date? = nil
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
|
|
|
|
|
private let monitor = NWPathMonitor()
|
|
|
|
|
private let queue = DispatchQueue(label: "com.albert.nasbackup.lan", qos: .utility)
|
|
|
|
|
|
|
|
|
|
var onTrustedNetworkJoined: ((String) -> Void)?
|
|
|
|
|
|
|
|
|
|
private init() {}
|
|
|
|
|
|
|
|
|
|
func start() {
|
|
|
|
|
monitor.pathUpdateHandler = { [weak self] path in
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
Task { @MainActor in
|
Add cellular data toggle, Tailscale remote access, fix ToggleRow padding
- ToggleRow: fix cramped layout — proper horizontal + vertical card padding
- ConnectionStore: add allowCellularBackup, useTailscaleWhenRemote, tailscaleHost
- LANMonitor: add isOnCellular (NWPath cellular interface), isTailscaleActive
(detects utun interface with 100.x.x.x CGNAT address), openTailscaleApp()
- BackupEngine: gate backup on cellular setting; resolveHost() switches to
tailscaleHost when not on trusted LAN and Tailscale tunnel is active
- SettingsView: CONNECTIVITY section (cellular toggle + live status),
REMOTE ACCESS section (Tailscale toggle, host field, status dot, Open button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:40:07 +03:00
|
|
|
self.isOnNetwork = path.status == .satisfied
|
|
|
|
|
self.isOnCellular = path.usesInterfaceType(.cellular)
|
|
|
|
|
self.isTailscaleActive = self.detectTailscale()
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
|
Add cellular data toggle, Tailscale remote access, fix ToggleRow padding
- ToggleRow: fix cramped layout — proper horizontal + vertical card padding
- ConnectionStore: add allowCellularBackup, useTailscaleWhenRemote, tailscaleHost
- LANMonitor: add isOnCellular (NWPath cellular interface), isTailscaleActive
(detects utun interface with 100.x.x.x CGNAT address), openTailscaleApp()
- BackupEngine: gate backup on cellular setting; resolveHost() switches to
tailscaleHost when not on trusted LAN and Tailscale tunnel is active
- SettingsView: CONNECTIVITY section (cellular toggle + live status),
REMOTE ACCESS section (Tailscale toggle, host field, status dot, Open button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:40:07 +03:00
|
|
|
if path.status == .satisfied {
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
await self.checkSSID()
|
|
|
|
|
} else {
|
|
|
|
|
self.currentSSID = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
monitor.start(queue: queue)
|
|
|
|
|
}
|
|
|
|
|
|
Add cellular data toggle, Tailscale remote access, fix ToggleRow padding
- ToggleRow: fix cramped layout — proper horizontal + vertical card padding
- ConnectionStore: add allowCellularBackup, useTailscaleWhenRemote, tailscaleHost
- LANMonitor: add isOnCellular (NWPath cellular interface), isTailscaleActive
(detects utun interface with 100.x.x.x CGNAT address), openTailscaleApp()
- BackupEngine: gate backup on cellular setting; resolveHost() switches to
tailscaleHost when not on trusted LAN and Tailscale tunnel is active
- SettingsView: CONNECTIVITY section (cellular toggle + live status),
REMOTE ACCESS section (Tailscale toggle, host field, status dot, Open button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:40:07 +03:00
|
|
|
func stop() { monitor.cancel() }
|
|
|
|
|
|
|
|
|
|
// Detect Tailscale: look for a utun interface with an IP in the 100.64.0.0/10 CGNAT range
|
|
|
|
|
private func detectTailscale() -> Bool {
|
|
|
|
|
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
|
|
|
|
guard getifaddrs(&ifaddr) == 0 else { return false }
|
|
|
|
|
defer { freeifaddrs(ifaddr) }
|
|
|
|
|
var ptr = ifaddr
|
|
|
|
|
while let current = ptr {
|
|
|
|
|
defer { ptr = current.pointee.ifa_next }
|
|
|
|
|
let name = String(cString: current.pointee.ifa_name)
|
|
|
|
|
guard name.hasPrefix("utun"),
|
|
|
|
|
let addr = current.pointee.ifa_addr,
|
|
|
|
|
addr.pointee.sa_family == UInt8(AF_INET) else { continue }
|
|
|
|
|
var sa = addr.pointee
|
|
|
|
|
var buf = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
|
|
|
getnameinfo(&sa, socklen_t(sa.sa_len), &buf, socklen_t(buf.count), nil, 0, NI_NUMERICHOST)
|
|
|
|
|
if String(cString: buf).hasPrefix("100.") { return true }
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open the Tailscale app so the user can connect the VPN
|
|
|
|
|
func openTailscaleApp() {
|
|
|
|
|
guard let url = URL(string: "tailscale://") else { return }
|
|
|
|
|
UIApplication.shared.open(url)
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
}
|
|
|
|
|
|
feat: Sync tab, hamburger menu, network status, browse UX
- Replace History tab with Sync tab (file list, NAS status, free-up-space retention)
- Add AppMenuView hamburger sheet (History, Errors, Connection Status, Sync Activity)
- BackupView: Kisani logo chip in toolbar, hamburger menu button, swipeable ring
pages (progress/pending/failed/uploaded), always-visible compact stats strip
- SettingsView: NETWORK section (SSID, NAS reachability, trusted-network warning,
refresh button)
- BrowseView: pull-to-refresh, long-press context menu on items
- LANMonitor: nasReachable, nasCheckTime, checkNASReachability(host:port:)
- ConnectionStore: localRetentionDays (0/60/90/365 days)
- Regenerate xcodeproj with xcodegen to include new files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 17:58:00 +03:00
|
|
|
func checkSSID() async {
|
Add cellular data toggle, Tailscale remote access, fix ToggleRow padding
- ToggleRow: fix cramped layout — proper horizontal + vertical card padding
- ConnectionStore: add allowCellularBackup, useTailscaleWhenRemote, tailscaleHost
- LANMonitor: add isOnCellular (NWPath cellular interface), isTailscaleActive
(detects utun interface with 100.x.x.x CGNAT address), openTailscaleApp()
- BackupEngine: gate backup on cellular setting; resolveHost() switches to
tailscaleHost when not on trusted LAN and Tailscale tunnel is active
- SettingsView: CONNECTIVITY section (cellular toggle + live status),
REMOTE ACCESS section (Tailscale toggle, host field, status dot, Open button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 11:40:07 +03:00
|
|
|
currentSSID = await fetchCurrentSSID()
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchCurrentSSID() async -> String? {
|
|
|
|
|
await withCheckedContinuation { continuation in
|
|
|
|
|
NEHotspotNetwork.fetchCurrent { network in
|
|
|
|
|
continuation.resume(returning: network?.ssid)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Sync tab, hamburger menu, network status, browse UX
- Replace History tab with Sync tab (file list, NAS status, free-up-space retention)
- Add AppMenuView hamburger sheet (History, Errors, Connection Status, Sync Activity)
- BackupView: Kisani logo chip in toolbar, hamburger menu button, swipeable ring
pages (progress/pending/failed/uploaded), always-visible compact stats strip
- SettingsView: NETWORK section (SSID, NAS reachability, trusted-network warning,
refresh button)
- BrowseView: pull-to-refresh, long-press context menu on items
- LANMonitor: nasReachable, nasCheckTime, checkNASReachability(host:port:)
- ConnectionStore: localRetentionDays (0/60/90/365 days)
- Regenerate xcodeproj with xcodegen to include new files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 17:58:00 +03:00
|
|
|
func checkNASReachability(host: String, port: Int) async {
|
|
|
|
|
let ep = NWEndpoint.hostPort(
|
|
|
|
|
host: NWEndpoint.Host(host),
|
|
|
|
|
port: NWEndpoint.Port(rawValue: UInt16(port)) ?? 445
|
|
|
|
|
)
|
|
|
|
|
let connection = NWConnection(to: ep, using: .tcp)
|
|
|
|
|
var done = false
|
|
|
|
|
|
|
|
|
|
let reachable = await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
|
|
|
|
|
connection.stateUpdateHandler = { state in
|
|
|
|
|
guard !done else { return }
|
|
|
|
|
switch state {
|
|
|
|
|
case .ready:
|
|
|
|
|
done = true
|
|
|
|
|
connection.cancel()
|
|
|
|
|
cont.resume(returning: true)
|
|
|
|
|
case .failed, .waiting:
|
|
|
|
|
done = true
|
|
|
|
|
connection.cancel()
|
|
|
|
|
cont.resume(returning: false)
|
|
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
connection.start(queue: queue)
|
|
|
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + 4) {
|
|
|
|
|
guard !done else { return }
|
|
|
|
|
done = true
|
|
|
|
|
connection.cancel()
|
|
|
|
|
cont.resume(returning: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nasReachable = reachable
|
|
|
|
|
nasCheckTime = Date()
|
|
|
|
|
}
|
|
|
|
|
|
Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors,
SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2),
PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager,
all feature UIs (Login, Connect, Browse, Backup, History, Settings),
Shared components and theme. Builds clean with zero errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:04 +03:00
|
|
|
func checkAndTriggerIfTrusted(trustedSSIDs: [String]) async {
|
|
|
|
|
let ssid = await fetchCurrentSSID()
|
|
|
|
|
guard let ssid, trustedSSIDs.contains(ssid) else { return }
|
|
|
|
|
onTrustedNetworkJoined?(ssid)
|
|
|
|
|
}
|
|
|
|
|
}
|