     1→//
     2→//  GamificationService.swift
     3→//  Planner2026-iOS
     4→//
     5→//  XP awards, badge unlocks, and level system management
     6→//
     7→
     8→import Foundation
     9→import Combine
    10→
    11→class GamificationService: ObservableObject {
    12→    static let shared = GamificationService()
    13→
    14→    // MARK: - Published Properties
    15→
    16→    @Published var userProgress: UserProgress
    17→    @Published var recentXPEvents: [XPEvent] = []
    18→    @Published var unlockedBadge: Badge?
    19→    @Published var levelUpNotification: Int?
    20→    @Published var lastXPEvent: XPEvent?
    21→
    22→    // MARK: - Persistence Keys
    23→
    24→    private let progressKey = "Planner2026_UserProgress"
    25→    private let xpEventsKey = "Planner2026_XPEvents"
    26→
    27→    // MARK: - Initialization
    28→
    29→    private init() {
    30→        self.userProgress = UserProgress()
    31→        loadProgress()
    32→    }
    33→
    34→    // MARK: - XP Management
    35→
    36→    func awardXP(_ amount: Int, reason: String, category: XPCategory) {
    37→        let event = XPEvent(amount: amount, reason: reason, category: category)
    38→        recentXPEvents.insert(event, at: 0)
    39→        lastXPEvent = event
    40→
    41→        // Keep only recent events within limit
    42→        if recentXPEvents.count > DataLimits.maxRecentXPEvents {
    43→            recentXPEvents = Array(recentXPEvents.prefix(DataLimits.maxRecentXPEvents))
    44→        }
    45→
    46→        let previousLevel = userProgress.level
    47→        userProgress.addXP(amount)
    48→
    49→        // Check for level-up
    50→        if userProgress.level > previousLevel {
    51→            levelUpNotification = userProgress.level
    52→            checkLevelBadges()
    53→            HapticManager.notification(.success)
    54→        }
    55→
    56→        saveProgress()
    57→    }
    58→
    59→    // Clear notifications after displaying
    60→    func clearLevelUpNotification() {
    61→        levelUpNotification = nil
    62→    }
    63→
    64→    func clearBadgeNotification() {
    65→        unlockedBadge = nil
    66→    }
    67→
    68→    func clearXPNotification() {
    69→        lastXPEvent = nil
    70→    }
    71→
    72→    // MARK: - SAVERS Rewards
    73→
    74→    func awardSAVERSElement(_ element: SAVERSElement) {
    75→        awardXP(
    76→            XPRewards.saversElement,
    77→            reason: "Completed \(element.rawValue)",
    78→            category: .morningRitual
    79→        )
    80→    }
    81→
    82→    func awardMorningRitualComplete(allSAVERSCompleted: Bool = false) {
    83→        awardXP(
    84→            XPRewards.morningRitualComplete,
    85→            reason: "Completed morning ritual",
    86→            category: .morningRitual
    87→        )
    88→
    89→        userProgress.streaks.updateMorningRitualStreak()
    90→        awardStreakBonus(userProgress.streaks.morningRitual)
    91→        checkMorningRitualBadges()
    92→
    93→        // Check for SAVERS Master badge
    94→        if allSAVERSCompleted && !hasBadge(.saversMaster) {
    95→            unlockBadge(.saversMaster)
    96→        }
    97→
    98→        saveProgress()
    99→    }
   100→
   101→    // MARK: - Walking Rewards
   102→
   103→    func awardWalkingSession(duration: Int, newLocations: Int, totalDistance: Double = 0, totalExploredLocations: Int = 0) {
   104→        awardXP(
   105→            XPRewards.walkingSession,
   106→            reason: "Completed \(duration) min walk",
   107→            category: .walking
   108→        )
   109→
   110→        if newLocations > 0 {
   111→            awardXP(
   112→                XPRewards.newLocation * newLocations,
   113→                reason: "Discovered \(newLocations) new location(s)",
   114→                category: .walking
   115→            )
   116→        }
   117→
   118→        userProgress.streaks.updateWalkingStreak()
   119→        awardStreakBonus(userProgress.streaks.walking)
   120→        checkWalkingBadges(totalDistance: totalDistance, totalExploredLocations: totalExploredLocations)
   121→        saveProgress()
   122→    }
   123→
   124→    // MARK: - Routine Rewards
   125→
   126→    func awardRoutineComplete() {
   127→        awardXP(
   128→            XPRewards.routineComplete,
   129→            reason: "Completed daily routine",
   130→            category: .routine
   131→        )
   132→
   133→        userProgress.streaks.updateRoutineStreak()
   134→        awardStreakBonus(userProgress.streaks.routine)
   135→        saveProgress()
   136→    }
   137→
   138→    // MARK: - Goals Rewards
   139→
   140→    func awardGoalProgress(goal: YearlyGoal, amount: Int) {
   141→        awardXP(
   142→            XPRewards.goalProgress * amount,
   143→            reason: "Progress on \(goal.title): +\(amount)",
   144→            category: .goals
   145→        )
   146→    }
   147→
   148→    func awardMilestoneReached(milestone: Milestone, goal: YearlyGoal) {
   149→        awardXP(
   150→            XPRewards.milestoneReached,
   151→            reason: "Reached milestone: \(milestone.title) on \(goal.title)",
   152→            category: .goals
   153→        )
   154→        checkGoalBadges(goal: goal)
   155→    }
   156→
   157→    // MARK: - Task Rewards
   158→
   159→    func awardTaskCompleted() {
   160→        awardXP(
   161→            XPRewards.taskCompleted,
   162→            reason: "Task completed",
   163→            category: .tasks
   164→        )
   165→    }
   166→
   167→    func awardProjectCompleted(name: String) {
   168→        awardXP(
   169→            XPRewards.projectCompleted,
   170→            reason: "Project completed: \(name)",
   171→            category: .tasks
   172→        )
   173→    }
   174→
   175→    // MARK: - Streak Bonus
   176→
   177→    private func awardStreakBonus(_ streakDays: Int) {
   178→        if streakDays > 1 {
   179→            let bonus = XPRewards.streakBonus * streakDays
   180→            awardXP(
   181→                bonus,
   182→                reason: "\(streakDays)-day streak bonus",
   183→                category: .streak
   184→            )
   185→        }
   186→        checkStreakBadges()
   187→    }
   188→
   189→    // MARK: - Badge Checks
   190→
   191→    private func checkMorningRitualBadges() {
   192→        let streak = userProgress.streaks.morningRitual
   193→
   194→        if streak == 1 && !hasBadge(.firstMorning) {
   195→            unlockBadge(.firstMorning)
   196→        }
   197→        if streak >= GamificationThresholds.weeklyStreakDays && !hasBadge(.weekMorning) {
   198→            unlockBadge(.weekMorning)
   199→        }
   200→        if streak >= GamificationThresholds.monthlyStreakDays && !hasBadge(.monthMorning) {
   201→            unlockBadge(.monthMorning)
   202→        }
   203→    }
   204→
   205→    private func checkWalkingBadges(totalDistance: Double = 0, totalExploredLocations: Int = 0) {
   206→        let streak = userProgress.streaks.walking
   207→
   208→        // Streak-based badges
   209→        if streak == 1 && !hasBadge(.firstWalk) {
   210→            unlockBadge(.firstWalk)
   211→        }
   212→        if streak >= GamificationThresholds.weeklyStreakDays && !hasBadge(.weekWalking) {
   213→            unlockBadge(.weekWalking)
   214→        }
   215→
   216→        // Explorer badge - discovered locations threshold
   217→        if totalExploredLocations >= GamificationThresholds.explorerLocations && !hasBadge(.explorer) {
   218→            unlockBadge(.explorer)
   219→        }
   220→
   221→        // Marathoner badge - total distance threshold (42km)
   222→        if totalDistance >= GamificationThresholds.marathonerDistance && !hasBadge(.marathoner) {
   223→            unlockBadge(.marathoner)
   224→        }
   225→    }
   226→
   227→    private func checkStreakBadges() {
   228→        let maxStreak = max(
   229→            userProgress.streaks.morningRitual,
   230→            userProgress.streaks.walking,
   231→            userProgress.streaks.routine
   232→        )
   233→
   234→        if maxStreak >= 3 && !hasBadge(.streakStarter) {
   235→            unlockBadge(.streakStarter)
   236→        }
   237→        if maxStreak >= 14 && !hasBadge(.streakWarrior) {
   238→            unlockBadge(.streakWarrior)
   239→        }
   240→        if maxStreak >= GamificationThresholds.monthlyStreakDays && !hasBadge(.streakLegend) {
   241→            unlockBadge(.streakLegend)
   242→        }
   243→    }
   244→
   245→    private func checkLevelBadges() {
   246→        if userProgress.level >= 5 && !hasBadge(.level5) {
   247→            unlockBadge(.level5)
   248→        }
   249→        if userProgress.level >= 10 && !hasBadge(.level10) {
   250→            unlockBadge(.level10)
   251→        }
   252→        if userProgress.level >= 25 && !hasBadge(.level25) {
   253→            unlockBadge(.level25)
   254→        }
   255→        if userProgress.level >= 50 && !hasBadge(.level50) {
   256→            unlockBadge(.level50)
   257→        }
   258→    }
   259→
   260→    private func checkGoalBadges(goal: YearlyGoal) {
   261→        // Check for iOS apps goal badges
   262→        if goal.title.lowercased().contains("ios") || goal.title.lowercased().contains("app") {
   263→            if goal.currentValue >= 10 && !hasBadge(.tenApps) {
   264→                unlockBadge(.tenApps)
   265→            }
   266→            if goal.currentValue >= 50 && !hasBadge(.fiftyApps) {
   267→                unlockBadge(.fiftyApps)
   268→            }
   269→            if goal.currentValue >= 100 && !hasBadge(.hundredApps) {
   270→                unlockBadge(.hundredApps)
   271→            }
   272→        }
   273→
   274→        // First goal badge
   275→        if !hasBadge(.firstGoal) {
   276→            unlockBadge(.firstGoal)
   277→        }
   278→    }
   279→
   280→    // MARK: - Badge Management
   281→
   282→    private func hasBadge(_ type: BadgeType) -> Bool {
   283→        userProgress.badges.contains { $0.type == type }
   284→    }
   285→
   286→    private func unlockBadge(_ type: BadgeType) {
   287→        let badge = Badge(type: type)
   288→        userProgress.badges.append(badge)
   289→        unlockedBadge = badge
   290→
   291→        // Award XP for badge unlock
   292→        awardXP(
   293→            50,
   294→            reason: "Unlocked badge: \(badge.name)",
   295→            category: .achievement
   296→        )
   297→
   298→        HapticManager.notification(.success)
   299→        saveProgress()
   300→    }
   301→
   302→    // MARK: - Persistence
   303→
   304→    private func loadProgress() {
   305→        if let data = UserDefaults.standard.data(forKey: progressKey),
   306→           let progress = try? JSONDecoder().decode(UserProgress.self, from: data) {
   307→            self.userProgress = progress
   308→        }
   309→
   310→        if let data = UserDefaults.standard.data(forKey: xpEventsKey),
   311→           let events = try? JSONDecoder().decode([XPEvent].self, from: data) {
   312→            self.recentXPEvents = events
   313→        }
   314→    }
   315→
   316→    private func saveProgress() {
   317→        if let data = try? JSONEncoder().encode(userProgress) {
   318→            UserDefaults.standard.set(data, forKey: progressKey)
   319→        }
   320→
   321→        if let data = try? JSONEncoder().encode(recentXPEvents) {
   322→            UserDefaults.standard.set(data, forKey: xpEventsKey)
   323→        }
   324→    }
   325→
   326→    // MARK: - Statistics
   327→
   328→    func getTodayXP() -> Int {
   329→        let today = Calendar.current.startOfDay(for: Date())
   330→        return recentXPEvents
   331→            .filter { $0.timestamp >= today }
   332→            .reduce(0) { $0 + $1.amount }
   333→    }
   334→
   335→    func getWeekXP() -> Int {
   336→        let weekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
   337→        return recentXPEvents
   338→            .filter { $0.timestamp >= weekAgo }
   339→            .reduce(0) { $0 + $1.amount }
   340→    }
   341→
   342→    func getXPByCategory(_ category: XPCategory) -> Int {
   343→        recentXPEvents
   344→            .filter { $0.category == category }
   345→            .reduce(0) { $0 + $1.amount }
   346→    }
   347→}
   348→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  GamificationService.swift
//  Planner2026-iOS
//
//  XP awards, badge unlocks, and level system management
//

import Foundation
import Combine

class GamificationService: ObservableObject {
    static let shared = GamificationService()

    // MARK: - Published Properties

    @Published var userProgress: UserProgress
    @Published var recentXPEvents: [XPEvent] = []
    @Published var unlockedBadge: Badge?
    @Published var levelUpNotification: Int?
    @Published var lastXPEvent: XPEvent?

    // MARK: - Persistence Keys

    private let progressKey = "Planner2026_UserProgress"
    private let xpEventsKey = "Planner2026_XPEvents"

    // MARK: - Initialization

    private init() {
        self.userProgress = UserProgress()
        loadProgress()
    }

    // MARK: - XP Management

    func awardXP(_ amount: Int, reason: String, category: XPCategory) {
        let event = XPEvent(amount: amount, reason: reason, category: category)
        recentXPEvents.insert(event, at: 0)
        lastXPEvent = event

        // Keep only recent events within limit
        if recentXPEvents.count > DataLimits.maxRecentXPEvents {
            recentXPEvents = Array(recentXPEvents.prefix(DataLimits.maxRecentXPEvents))
        }

        let previousLevel = userProgress.level
        userProgress.addXP(amount)

        // Check for level-up
        if userProgress.level > previousLevel {
            levelUpNotification = userProgress.level
            checkLevelBadges()
            HapticManager.notification(.success)
        }

        saveProgress()
    }

    // Clear notifications after displaying
    func clearLevelUpNotification() {
        levelUpNotification = nil
    }

    func clearBadgeNotification() {
        unlockedBadge = nil
    }

    func clearXPNotification() {
        lastXPEvent = nil
    }

    // MARK: - SAVERS Rewards

    func awardSAVERSElement(_ element: SAVERSElement) {
        awardXP(
            XPRewards.saversElement,
            reason: "Completed \(element.rawValue)",
            category: .morningRitual
        )
    }

    func awardMorningRitualComplete(allSAVERSCompleted: Bool = false) {
        awardXP(
            XPRewards.morningRitualComplete,
            reason: "Completed morning ritual",
            category: .morningRitual
        )

        userProgress.streaks.updateMorningRitualStreak()
        awardStreakBonus(userProgress.streaks.morningRitual)
        checkMorningRitualBadges()

        // Check for SAVERS Master badge
        if allSAVERSCompleted && !hasBadge(.saversMaster) {
            unlockBadge(.saversMaster)
        }

        saveProgress()
    }

    // MARK: - Walking Rewards

    func awardWalkingSession(duration: Int, newLocations: Int, totalDistance: Double = 0, totalExploredLocations: Int = 0) {
        awardXP(
            XPRewards.walkingSession,
            reason: "Completed \(duration) min walk",
            category: .walking
        )

        if newLocations > 0 {
            awardXP(
                XPRewards.newLocation * newLocations,
                reason: "Discovered \(newLocations) new location(s)",
                category: .walking
            )
        }

        userProgress.streaks.updateWalkingStreak()
        awardStreakBonus(userProgress.streaks.walking)
        checkWalkingBadges(totalDistance: totalDistance, totalExploredLocations: totalExploredLocations)
        saveProgress()
    }

    // MARK: - Routine Rewards

    func awardRoutineComplete() {
        awardXP(
            XPRewards.routineComplete,
            reason: "Completed daily routine",
            category: .routine
        )

        userProgress.streaks.updateRoutineStreak()
        awardStreakBonus(userProgress.streaks.routine)
        saveProgress()
    }

    // MARK: - Goals Rewards

    func awardGoalProgress(goal: YearlyGoal, amount: Int) {
        awardXP(
            XPRewards.goalProgress * amount,
            reason: "Progress on \(goal.title): +\(amount)",
            category: .goals
        )
    }

    func awardMilestoneReached(milestone: Milestone, goal: YearlyGoal) {
        awardXP(
            XPRewards.milestoneReached,
            reason: "Reached milestone: \(milestone.title) on \(goal.title)",
            category: .goals
        )
        checkGoalBadges(goal: goal)
    }

    // MARK: - Task Rewards

    func awardTaskCompleted() {
        awardXP(
            XPRewards.taskCompleted,
            reason: "Task completed",
            category: .tasks
        )
    }

    func awardProjectCompleted(name: String) {
        awardXP(
            XPRewards.projectCompleted,
            reason: "Project completed: \(name)",
            category: .tasks
        )
    }

    // MARK: - Streak Bonus

    private func awardStreakBonus(_ streakDays: Int) {
        if streakDays > 1 {
            let bonus = XPRewards.streakBonus * streakDays
            awardXP(
                bonus,
                reason: "\(streakDays)-day streak bonus",
                category: .streak
            )
        }
        checkStreakBadges()
    }

    // MARK: - Badge Checks

    private func checkMorningRitualBadges() {
        let streak = userProgress.streaks.morningRitual

        if streak == 1 && !hasBadge(.firstMorning) {
            unlockBadge(.firstMorning)
        }
        if streak >= GamificationThresholds.weeklyStreakDays && !hasBadge(.weekMorning) {
            unlockBadge(.weekMorning)
        }
        if streak >= GamificationThresholds.monthlyStreakDays && !hasBadge(.monthMorning) {
            unlockBadge(.monthMorning)
        }
    }

    private func checkWalkingBadges(totalDistance: Double = 0, totalExploredLocations: Int = 0) {
        let streak = userProgress.streaks.walking

        // Streak-based badges
        if streak == 1 && !hasBadge(.firstWalk) {
            unlockBadge(.firstWalk)
        }
        if streak >= GamificationThresholds.weeklyStreakDays && !hasBadge(.weekWalking) {
            unlockBadge(.weekWalking)
        }

        // Explorer badge - discovered locations threshold
        if totalExploredLocations >= GamificationThresholds.explorerLocations && !hasBadge(.explorer) {
            unlockBadge(.explorer)
        }

        // Marathoner badge - total distance threshold (42km)
        if totalDistance >= GamificationThresholds.marathonerDistance && !hasBadge(.marathoner) {
            unlockBadge(.marathoner)
        }
    }

    private func checkStreakBadges() {
        let maxStreak = max(
            userProgress.streaks.morningRitual,
            userProgress.streaks.walking,
            userProgress.streaks.routine
        )

        if maxStreak >= 3 && !hasBadge(.streakStarter) {
            unlockBadge(.streakStarter)
        }
        if maxStreak >= 14 && !hasBadge(.streakWarrior) {
            unlockBadge(.streakWarrior)
        }
        if maxStreak >= GamificationThresholds.monthlyStreakDays && !hasBadge(.streakLegend) {
            unlockBadge(.streakLegend)
        }
    }

    private func checkLevelBadges() {
        if userProgress.level >= 5 && !hasBadge(.level5) {
            unlockBadge(.level5)
        }
        if userProgress.level >= 10 && !hasBadge(.level10) {
            unlockBadge(.level10)
        }
        if userProgress.level >= 25 && !hasBadge(.level25) {
            unlockBadge(.level25)
        }
        if userProgress.level >= 50 && !hasBadge(.level50) {
            unlockBadge(.level50)
        }
    }

    private func checkGoalBadges(goal: YearlyGoal) {
        // Check for iOS apps goal badges
        if goal.title.lowercased().contains("ios") || goal.title.lowercased().contains("app") {
            if goal.currentValue >= 10 && !hasBadge(.tenApps) {
                unlockBadge(.tenApps)
            }
            if goal.currentValue >= 50 && !hasBadge(.fiftyApps) {
                unlockBadge(.fiftyApps)
            }
            if goal.currentValue >= 100 && !hasBadge(.hundredApps) {
                unlockBadge(.hundredApps)
            }
        }

        // First goal badge
        if !hasBadge(.firstGoal) {
            unlockBadge(.firstGoal)
        }
    }

    // MARK: - Badge Management

    private func hasBadge(_ type: BadgeType) -> Bool {
        userProgress.badges.contains { $0.type == type }
    }

    private func unlockBadge(_ type: BadgeType) {
        let badge = Badge(type: type)
        userProgress.badges.append(badge)
        unlockedBadge = badge

        // Award XP for badge unlock
        awardXP(
            50,
            reason: "Unlocked badge: \(badge.name)",
            category: .achievement
        )

        HapticManager.notification(.success)
        saveProgress()
    }

    // MARK: - Persistence

    private func loadProgress() {
        if let data = UserDefaults.standard.data(forKey: progressKey),
           let progress = try? JSONDecoder().decode(UserProgress.self, from: data) {
            self.userProgress = progress
        }

        if let data = UserDefaults.standard.data(forKey: xpEventsKey),
           let events = try? JSONDecoder().decode([XPEvent].self, from: data) {
            self.recentXPEvents = events
        }
    }

    private func saveProgress() {
        if let data = try? JSONEncoder().encode(userProgress) {
            UserDefaults.standard.set(data, forKey: progressKey)
        }

        if let data = try? JSONEncoder().encode(recentXPEvents) {
            UserDefaults.standard.set(data, forKey: xpEventsKey)
        }
    }

    // MARK: - Statistics

    func getTodayXP() -> Int {
        let today = Calendar.current.startOfDay(for: Date())
        return recentXPEvents
            .filter { $0.timestamp >= today }
            .reduce(0) { $0 + $1.amount }
    }

    func getWeekXP() -> Int {
        let weekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) ?? Date()
        return recentXPEvents
            .filter { $0.timestamp >= weekAgo }
            .reduce(0) { $0 + $1.amount }
    }

    func getXPByCategory(_ category: XPCategory) -> Int {
        recentXPEvents
            .filter { $0.category == category }
            .reduce(0) { $0 + $1.amount }
    }
}


     1→//
     2→//  ClaudeService.swift
     3→//  Planner2026-iOS
     4→//
     5→//  Claude API integration for AI features
     6→//
     7→
     8→import Foundation
     9→
    10→@MainActor
    11→class ClaudeService: ObservableObject {
    12→    static let shared = ClaudeService()
    13→
    14→    // MARK: - Published State
    15→    @Published var isLoading = false
    16→    @Published var error: String?
    17→
    18→    // MARK: - Configuration
    19→    private let baseURL = "https://api.anthropic.com/v1/messages"
    20→    private let anthropicVersion = "2023-06-01"
    21→
    22→    private var apiKey: String {
    23→        KeychainService.shared.claudeAPIKey ?? ""
    24→    }
    25→
    26→    // Models
    27→    enum ClaudeModel: String {
    28→        case sonnet = "claude-sonnet-4-20250514"
    29→        case opus = "claude-opus-4-20250514"
    30→    }
    31→
    32→    var preferredModel: ClaudeModel = .sonnet
    33→
    34→    // MARK: - Lazy URL Session
    35→    private lazy var session: URLSession = {
    36→        let config = URLSessionConfiguration.default
    37→        config.timeoutIntervalForRequest = 60
    38→        config.timeoutIntervalForResource = 120
    39→        return URLSession(configuration: config)
    40→    }()
    41→
    42→    private init() {}
    43→
    44→    // MARK: - Public Methods
    45→
    46→    func hasAPIKey() -> Bool {
    47→        !apiKey.isEmpty
    48→    }
    49→
    50→    func setAPIKey(_ key: String) {
    51→        KeychainService.shared.claudeAPIKey = key
    52→    }
    53→
    54→    /// Send a message to Claude and get a response
    55→    func sendMessage(_ message: String, systemPrompt: String? = nil) async throws -> String {
    56→        isLoading = true
    57→        error = nil
    58→        defer { isLoading = false }
    59→
    60→        guard !apiKey.isEmpty else {
    61→            throw ClaudeError.missingAPIKey
    62→        }
    63→
    64→        guard let url = URL(string: baseURL) else {
    65→            throw ClaudeError.invalidURL
    66→        }
    67→
    68→        var request = URLRequest(url: url)
    69→        request.httpMethod = "POST"
    70→        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    71→        request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
    72→        request.setValue(anthropicVersion, forHTTPHeaderField: "anthropic-version")
    73→
    74→        var body: [String: Any] = [
    75→            "model": preferredModel.rawValue,
    76→            "max_tokens": 4096,
    77→            "messages": [
    78→                ["role": "user", "content": message]
    79→            ]
    80→        ]
    81→
    82→        if let systemPrompt = systemPrompt {
    83→            body["system"] = systemPrompt
    84→        }
    85→
    86→        request.httpBody = try JSONSerialization.data(withJSONObject: body)
    87→
    88→        let data: Data
    89→        let response: URLResponse
    90→        do {
    91→            (data, response) = try await session.data(for: request)
    92→        } catch let urlError as URLError {
    93→            throw ClaudeError.networkError(urlError)
    94→        }
    95→
    96→        guard let httpResponse = response as? HTTPURLResponse else {
    97→            throw ClaudeError.invalidResponse
    98→        }
    99→
   100→        if httpResponse.statusCode != 200 {
   101→            let errorMessage = parseErrorMessage(from: data)
   102→            self.error = errorMessage
   103→            throw ClaudeError.apiError(statusCode: httpResponse.statusCode, message: errorMessage)
   104→        }
   105→
   106→        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
   107→              let content = json["content"] as? [[String: Any]],
   108→              let firstContent = content.first,
   109→              let text = firstContent["text"] as? String else {
   110→            throw ClaudeError.parsingError
   111→        }
   112→
   113→        HapticManager.aiResponse()
   114→        return text
   115→    }
   116→
   117→    /// Send with conversation history
   118→    func sendConversation(_ messages: [ChatMessage], systemPrompt: String? = nil) async throws -> String {
   119→        isLoading = true
   120→        error = nil
   121→        defer { isLoading = false }
   122→
   123→        guard !apiKey.isEmpty else {
   124→            throw ClaudeError.missingAPIKey
   125→        }
   126→
   127→        guard let url = URL(string: baseURL) else {
   128→            throw ClaudeError.invalidURL
   129→        }
   130→
   131→        var request = URLRequest(url: url)
   132→        request.httpMethod = "POST"
   133→        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
   134→        request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
   135→        request.setValue(anthropicVersion, forHTTPHeaderField: "anthropic-version")
   136→
   137→        // Convert messages to API format
   138→        let apiMessages = messages.filter { $0.role != .system && !$0.isLoading }.map { msg -> [String: String] in
   139→            ["role": msg.role.rawValue, "content": msg.content]
   140→        }
   141→
   142→        var body: [String: Any] = [
   143→            "model": preferredModel.rawValue,
   144→            "max_tokens": 4096,
   145→            "messages": apiMessages
   146→        ]
   147→
   148→        if let systemPrompt = systemPrompt {
   149→            body["system"] = systemPrompt
   150→        }
   151→
   152→        request.httpBody = try JSONSerialization.data(withJSONObject: body)
   153→
   154→        let data: Data
   155→        let response: URLResponse
   156→        do {
   157→            (data, response) = try await session.data(for: request)
   158→        } catch let urlError as URLError {
   159→            throw ClaudeError.networkError(urlError)
   160→        }
   161→
   162→        guard let httpResponse = response as? HTTPURLResponse else {
   163→            throw ClaudeError.invalidResponse
   164→        }
   165→
   166→        if httpResponse.statusCode != 200 {
   167→            let errorMessage = parseErrorMessage(from: data)
   168→            self.error = errorMessage
   169→            throw ClaudeError.apiError(statusCode: httpResponse.statusCode, message: errorMessage)
   170→        }
   171→
   172→        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
   173→              let content = json["content"] as? [[String: Any]],
   174→              let firstContent = content.first,
   175→              let text = firstContent["text"] as? String else {
   176→            throw ClaudeError.parsingError
   177→        }
   178→
   179→        HapticManager.aiResponse()
   180→        return text
   181→    }
   182→
   183→    private func parseErrorMessage(from data: Data) -> String {
   184→        if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
   185→           let error = json["error"] as? [String: Any],
   186→           let message = error["message"] as? String {
   187→            return message
   188→        }
   189→        return "Unknown error"
   190→    }
   191→}
   192→
   193→// MARK: - Errors
   194→enum ClaudeError: LocalizedError {
   195→    case missingAPIKey
   196→    case invalidURL
   197→    case invalidResponse
   198→    case parsingError
   199→    case apiError(statusCode: Int, message: String)
   200→    case networkError(URLError)
   201→
   202→    var errorDescription: String? {
   203→        switch self {
   204→        case .missingAPIKey:
   205→            return "Claude API key not configured"
   206→        case .invalidURL:
   207→            return "Invalid API URL"
   208→        case .invalidResponse:
   209→            return "Invalid response from server"
   210→        case .parsingError:
   211→            return "Failed to parse response"
   212→        case .apiError(let code, let message):
   213→            return "API Error (\(code)): \(message)"
   214→        case .networkError(let urlError):
   215→            switch urlError.code {
   216→            case .notConnectedToInternet:
   217→                return "No internet connection"
   218→            case .timedOut:
   219→                return "Request timed out"
   220→            case .networkConnectionLost:
   221→                return "Connection lost"
   222→            default:
   223→                return "Network error: \(urlError.localizedDescription)"
   224→            }
   225→        }
   226→    }
   227→}
   228→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  ClaudeService.swift
//  Planner2026-iOS
//
//  Claude API integration for AI features
//

import Foundation

@MainActor
class ClaudeService: ObservableObject {
    static let shared = ClaudeService()

    // MARK: - Published State
    @Published var isLoading = false
    @Published var error: String?

    // MARK: - Configuration
    private let baseURL = "https://api.anthropic.com/v1/messages"
    private let anthropicVersion = "2023-06-01"

    private var apiKey: String {
        KeychainService.shared.claudeAPIKey ?? ""
    }

    // Models
    enum ClaudeModel: String {
        case sonnet = "claude-sonnet-4-20250514"
        case opus = "claude-opus-4-20250514"
    }

    var preferredModel: ClaudeModel = .sonnet

    // MARK: - Lazy URL Session
    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 60
        config.timeoutIntervalForResource = 120
        return URLSession(configuration: config)
    }()

    private init() {}

    // MARK: - Public Methods

    func hasAPIKey() -> Bool {
        !apiKey.isEmpty
    }

    func setAPIKey(_ key: String) {
        KeychainService.shared.claudeAPIKey = key
    }

    /// Send a message to Claude and get a response
    func sendMessage(_ message: String, systemPrompt: String? = nil) async throws -> String {
        isLoading = true
        error = nil
        defer { isLoading = false }

        guard !apiKey.isEmpty else {
            throw ClaudeError.missingAPIKey
        }

        guard let url = URL(string: baseURL) else {
            throw ClaudeError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
        request.setValue(anthropicVersion, forHTTPHeaderField: "anthropic-version")

        var body: [String: Any] = [
            "model": preferredModel.rawValue,
            "max_tokens": 4096,
            "messages": [
                ["role": "user", "content": message]
            ]
        ]

        if let systemPrompt = systemPrompt {
            body["system"] = systemPrompt
        }

        request.httpBody = try JSONSerialization.data(withJSONObject: body)

        let data: Data
        let response: URLResponse
        do {
            (data, response) = try await session.data(for: request)
        } catch let urlError as URLError {
            throw ClaudeError.networkError(urlError)
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            throw ClaudeError.invalidResponse
        }

        if httpResponse.statusCode != 200 {
            let errorMessage = parseErrorMessage(from: data)
            self.error = errorMessage
            throw ClaudeError.apiError(statusCode: httpResponse.statusCode, message: errorMessage)
        }

        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
              let content = json["content"] as? [[String: Any]],
              let firstContent = content.first,
              let text = firstContent["text"] as? String else {
            throw ClaudeError.parsingError
        }

        HapticManager.aiResponse()
        return text
    }

    /// Send with conversation history
    func sendConversation(_ messages: [ChatMessage], systemPrompt: String? = nil) async throws -> String {
        isLoading = true
        error = nil
        defer { isLoading = false }

        guard !apiKey.isEmpty else {
            throw ClaudeError.missingAPIKey
        }

        guard let url = URL(string: baseURL) else {
            throw ClaudeError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
        request.setValue(anthropicVersion, forHTTPHeaderField: "anthropic-version")

        // Convert messages to API format
        let apiMessages = messages.filter { $0.role != .system && !$0.isLoading }.map { msg -> [String: String] in
            ["role": msg.role.rawValue, "content": msg.content]
        }

        var body: [String: Any] = [
            "model": preferredModel.rawValue,
            "max_tokens": 4096,
            "messages": apiMessages
        ]

        if let systemPrompt = systemPrompt {
            body["system"] = systemPrompt
        }

        request.httpBody = try JSONSerialization.data(withJSONObject: body)

        let data: Data
        let response: URLResponse
        do {
            (data, response) = try await session.data(for: request)
        } catch let urlError as URLError {
            throw ClaudeError.networkError(urlError)
        }

        guard let httpResponse = response as? HTTPURLResponse else {
            throw ClaudeError.invalidResponse
        }

        if httpResponse.statusCode != 200 {
            let errorMessage = parseErrorMessage(from: data)
            self.error = errorMessage
            throw ClaudeError.apiError(statusCode: httpResponse.statusCode, message: errorMessage)
        }

        guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
              let content = json["content"] as? [[String: Any]],
              let firstContent = content.first,
              let text = firstContent["text"] as? String else {
            throw ClaudeError.parsingError
        }

        HapticManager.aiResponse()
        return text
    }

    private func parseErrorMessage(from data: Data) -> String {
        if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
           let error = json["error"] as? [String: Any],
           let message = error["message"] as? String {
            return message
        }
        return "Unknown error"
    }
}

// MARK: - Errors
enum ClaudeError: LocalizedError {
    case missingAPIKey
    case invalidURL
    case invalidResponse
    case parsingError
    case apiError(statusCode: Int, message: String)
    case networkError(URLError)

    var errorDescription: String? {
        switch self {
        case .missingAPIKey:
            return "Claude API key not configured"
        case .invalidURL:
            return "Invalid API URL"
        case .invalidResponse:
            return "Invalid response from server"
        case .parsingError:
            return "Failed to parse response"
        case .apiError(let code, let message):
            return "API Error (\(code)): \(message)"
        case .networkError(let urlError):
            switch urlError.code {
            case .notConnectedToInternet:
                return "No internet connection"
            case .timedOut:
                return "Request timed out"
            case .networkConnectionLost:
                return "Connection lost"
            default:
                return "Network error: \(urlError.localizedDescription)"
            }
        }
    }
}


     1→//
     2→//  AIModels.swift
     3→//  Planner2026-iOS
     4→//
     5→//  Models for AI chat, insights, and scheduling
     6→//
     7→
     8→import Foundation
     9→
    10→// MARK: - Chat Models
    11→
    12→enum ChatRole: String, Codable {
    13→    case user
    14→    case assistant
    15→    case system
    16→}
    17→
    18→struct ChatMessage: Identifiable, Codable, Hashable {
    19→    var id: UUID = UUID()
    20→    var role: ChatRole
    21→    var content: String
    22→    var timestamp: Date = Date()
    23→    var isLoading: Bool = false
    24→
    25→    // For non-Codable isLoading
    26→    enum CodingKeys: String, CodingKey {
    27→        case id, role, content, timestamp
    28→    }
    29→}
    30→
    31→struct AIConversation: Identifiable, Codable, Hashable {
    32→    var id: UUID = UUID()
    33→    var title: String = "New Chat"
    34→    var messages: [ChatMessage] = []
    35→    var createdAt: Date = Date()
    36→    var modifiedAt: Date = Date()
    37→
    38→    var preview: String {
    39→        messages.last(where: { $0.role == .user })?.content.prefix(50).description ?? "No messages"
    40→    }
    41→
    42→    var messageCount: Int {
    43→        messages.filter { $0.role != .system }.count
    44→    }
    45→}
    46→
    47→// MARK: - Insight Models
    48→
    49→enum InsightType: String, Codable {
    50→    case productivity       // Overall productivity
    51→    case scheduling         // Best times to work
    52→    case completion         // Task completion patterns
    53→    case location           // Location-based insights
    54→    case recommendation     // AI recommendations
    55→}
    56→
    57→struct AIInsight: Identifiable, Codable, Hashable {
    58→    var id: UUID = UUID()
    59→    var type: InsightType
    60→    var title: String
    61→    var content: String
    62→    var confidence: Double  // 0-1
    63→    var createdAt: Date = Date()
    64→    var expiresAt: Date?
    65→    var isRead: Bool = false
    66→
    67→    var isExpired: Bool {
    68→        guard let expires = expiresAt else { return false }
    69→        return expires < Date()
    70→    }
    71→
    72→    var confidenceLabel: String {
    73→        switch confidence {
    74→        case 0.9...1.0: return "High"
    75→        case 0.7..<0.9: return "Medium"
    76→        case 0.5..<0.7: return "Low"
    77→        default: return "Very Low"
    78→        }
    79→    }
    80→
    81→    var icon: String {
    82→        switch type {
    83→        case .productivity: return "chart.line.uptrend.xyaxis"
    84→        case .scheduling: return "clock.fill"
    85→        case .completion: return "checkmark.circle.fill"
    86→        case .location: return "location.fill"
    87→        case .recommendation: return "lightbulb.fill"
    88→        }
    89→    }
    90→}
    91→
    92→// MARK: - Scheduling Models
    93→
    94→struct ScheduledBlock: Identifiable, Codable, Hashable {
    95→    var id: UUID = UUID()
    96→    var taskId: UUID?
    97→    var title: String
    98→    var startTime: Date
    99→    var endTime: Date
   100→    var confidence: Double  // 0-1
   101→    var isBreak: Bool = false
   102→
   103→    var duration: TimeInterval {
   104→        endTime.timeIntervalSince(startTime)
   105→    }
   106→
   107→    var durationMinutes: Int {
   108→        Int(duration / 60)
   109→    }
   110→
   111→    var timeRange: String {
   112→        let formatter = DateFormatter()
   113→        formatter.timeStyle = .short
   114→        return "\(formatter.string(from: startTime)) - \(formatter.string(from: endTime))"
   115→    }
   116→}
   117→
   118→struct DaySchedule: Identifiable, Codable, Hashable {
   119→    var id: UUID = UUID()
   120→    var date: Date
   121→    var blocks: [ScheduledBlock] = []
   122→    var generatedAt: Date = Date()
   123→    var isAccepted: Bool = false
   124→
   125→    var totalWorkMinutes: Int {
   126→        blocks.filter { !$0.isBreak }.reduce(0) { $0 + $1.durationMinutes }
   127→    }
   128→
   129→    var totalBreakMinutes: Int {
   130→        blocks.filter { $0.isBreak }.reduce(0) { $0 + $1.durationMinutes }
   131→    }
   132→}
   133→
   134→// MARK: - Preview Support
   135→
   136→#if DEBUG
   137→extension ChatMessage {
   138→    static var preview: ChatMessage {
   139→        ChatMessage(role: .user, content: "Help me plan my day")
   140→    }
   141→
   142→    static var samples: [ChatMessage] {
   143→        [
   144→            ChatMessage(role: .user, content: "What should I focus on today?"),
   145→            ChatMessage(role: .assistant, content: "Based on your task list, I recommend starting with the urgent items...")
   146→        ]
   147→    }
   148→}
   149→
   150→extension AIConversation {
   151→    static var preview: AIConversation {
   152→        AIConversation(
   153→            title: "Daily Planning",
   154→            messages: ChatMessage.samples
   155→        )
   156→    }
   157→}
   158→
   159→extension AIInsight {
   160→    static var preview: AIInsight {
   161→        AIInsight(
   162→            type: .productivity,
   163→            title: "Peak Productivity Hours",
   164→            content: "You complete 40% more tasks between 9-11 AM. Consider scheduling important work during this window.",
   165→            confidence: 0.85
   166→        )
   167→    }
   168→
   169→    static var samples: [AIInsight] {
   170→        [
   171→            AIInsight(
   172→                type: .scheduling,
   173→                title: "Best Meeting Times",
   174→                content: "Your calendar shows fewer interruptions on Tuesdays and Thursdays.",
   175→                confidence: 0.78
   176→            ),
   177→            AIInsight(
   178→                type: .location,
   179→                title: "Work Location Pattern",
   180→                content: "You complete 25% more tasks when working from home.",
   181→                confidence: 0.72
   182→            )
   183→        ]
   184→    }
   185→}
   186→
   187→extension ScheduledBlock {
   188→    static var preview: ScheduledBlock {
   189→        ScheduledBlock(
   190→            taskId: UUID(),
   191→            title: "Complete project proposal",
   192→            startTime: Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: Date())!,
   193→            endTime: Calendar.current.date(bySettingHour: 10, minute: 30, second: 0, of: Date())!,
   194→            confidence: 0.85
   195→        )
   196→    }
   197→}
   198→#endif
   199→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  AIModels.swift
//  Planner2026-iOS
//
//  Models for AI chat, insights, and scheduling
//

import Foundation

// MARK: - Chat Models

enum ChatRole: String, Codable {
    case user
    case assistant
    case system
}

struct ChatMessage: Identifiable, Codable, Hashable {
    var id: UUID = UUID()
    var role: ChatRole
    var content: String
    var timestamp: Date = Date()
    var isLoading: Bool = false

    // For non-Codable isLoading
    enum CodingKeys: String, CodingKey {
        case id, role, content, timestamp
    }
}

struct AIConversation: Identifiable, Codable, Hashable {
    var id: UUID = UUID()
    var title: String = "New Chat"
    var messages: [ChatMessage] = []
    var createdAt: Date = Date()
    var modifiedAt: Date = Date()

    var preview: String {
        messages.last(where: { $0.role == .user })?.content.prefix(50).description ?? "No messages"
    }

    var messageCount: Int {
        messages.filter { $0.role != .system }.count
    }
}

// MARK: - Insight Models

enum InsightType: String, Codable {
    case productivity       // Overall productivity
    case scheduling         // Best times to work
    case completion         // Task completion patterns
    case location           // Location-based insights
    case recommendation     // AI recommendations
}

struct AIInsight: Identifiable, Codable, Hashable {
    var id: UUID = UUID()
    var type: InsightType
    var title: String
    var content: String
    var confidence: Double  // 0-1
    var createdAt: Date = Date()
    var expiresAt: Date?
    var isRead: Bool = false

    var isExpired: Bool {
        guard let expires = expiresAt else { return false }
        return expires < Date()
    }

    var confidenceLabel: String {
        switch confidence {
        case 0.9...1.0: return "High"
        case 0.7..<0.9: return "Medium"
        case 0.5..<0.7: return "Low"
        default: return "Very Low"
        }
    }

    var icon: String {
        switch type {
        case .productivity: return "chart.line.uptrend.xyaxis"
        case .scheduling: return "clock.fill"
        case .completion: return "checkmark.circle.fill"
        case .location: return "location.fill"
        case .recommendation: return "lightbulb.fill"
        }
    }
}

// MARK: - Scheduling Models

struct ScheduledBlock: Identifiable, Codable, Hashable {
    var id: UUID = UUID()
    var taskId: UUID?
    var title: String
    var startTime: Date
    var endTime: Date
    var confidence: Double  // 0-1
    var isBreak: Bool = false

    var duration: TimeInterval {
        endTime.timeIntervalSince(startTime)
    }

    var durationMinutes: Int {
        Int(duration / 60)
    }

    var timeRange: String {
        let formatter = DateFormatter()
        formatter.timeStyle = .short
        return "\(formatter.string(from: startTime)) - \(formatter.string(from: endTime))"
    }
}

struct DaySchedule: Identifiable, Codable, Hashable {
    var id: UUID = UUID()
    var date: Date
    var blocks: [ScheduledBlock] = []
    var generatedAt: Date = Date()
    var isAccepted: Bool = false

    var totalWorkMinutes: Int {
        blocks.filter { !$0.isBreak }.reduce(0) { $0 + $1.durationMinutes }
    }

    var totalBreakMinutes: Int {
        blocks.filter { $0.isBreak }.reduce(0) { $0 + $1.durationMinutes }
    }
}

// MARK: - Preview Support

#if DEBUG
extension ChatMessage {
    static var preview: ChatMessage {
        ChatMessage(role: .user, content: "Help me plan my day")
    }

    static var samples: [ChatMessage] {
        [
            ChatMessage(role: .user, content: "What should I focus on today?"),
            ChatMessage(role: .assistant, content: "Based on your task list, I recommend starting with the urgent items...")
        ]
    }
}

extension AIConversation {
    static var preview: AIConversation {
        AIConversation(
            title: "Daily Planning",
            messages: ChatMessage.samples
        )
    }
}

extension AIInsight {
    static var preview: AIInsight {
        AIInsight(
            type: .productivity,
            title: "Peak Productivity Hours",
            content: "You complete 40% more tasks between 9-11 AM. Consider scheduling important work during this window.",
            confidence: 0.85
        )
    }

    static var samples: [AIInsight] {
        [
            AIInsight(
                type: .scheduling,
                title: "Best Meeting Times",
                content: "Your calendar shows fewer interruptions on Tuesdays and Thursdays.",
                confidence: 0.78
            ),
            AIInsight(
                type: .location,
                title: "Work Location Pattern",
                content: "You complete 25% more tasks when working from home.",
                confidence: 0.72
            )
        ]
    }
}

extension ScheduledBlock {
    static var preview: ScheduledBlock {
        ScheduledBlock(
            taskId: UUID(),
            title: "Complete project proposal",
            startTime: Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: Date())!,
            endTime: Calendar.current.date(bySettingHour: 10, minute: 30, second: 0, of: Date())!,
            confidence: 0.85
        )
    }
}
#endif


Let me check the constants and a couple more key files:


Bash

find /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services -type f -name "*.swift" | xargs wc -l | sort -n


     1→//
     2→//  Constants.swift
     3→//  Planner2026-iOS
     4→//
     5→//  App-wide constants to avoid magic numbers
     6→//
     7→
     8→import Foundation
     9→
    10→// MARK: - Data Retention Limits
    11→
    12→enum DataLimits {
    13→    /// Maximum location history entries to keep
    14→    static let maxLocationHistory = 1000
    15→
    16→    /// Maximum AI conversations to retain
    17→    static let maxConversations = 50
    18→
    19→    /// Maximum AI insights to retain
    20→    static let maxInsights = 100
    21→
    22→    /// Maximum recent XP events to display
    23→    static let maxRecentXPEvents = 50
    24→
    25→    /// Days to retain daily schedules
    26→    static let schedulesRetentionDays = 30
    27→
    28→    /// Days to retain morning ritual history
    29→    static let morningRitualsRetentionDays = 90
    30→
    31→    /// Days to retain daily routine history
    32→    static let dailyRoutinesRetentionDays = 90
    33→}
    34→
    35→// MARK: - Walking Quest Thresholds
    36→
    37→enum WalkingThresholds {
    38→    /// Minimum minutes for short walk bonus
    39→    static let shortWalkMinutes = 15
    40→
    41→    /// Minimum minutes for long walk bonus
    42→    static let longWalkMinutes = 30
    43→
    44→    /// Default target minutes per day
    45→    static let defaultTargetMinutes = 30
    46→
    47→    /// Distance threshold for "nearby" locations in meters
    48→    static let nearbyRadius: Double = 500
    49→
    50→    /// Distance threshold for "explored" location in meters
    51→    static let exploredRadius: Double = 50
    52→
    53→    /// Minimum location accuracy in meters
    54→    static let minLocationAccuracy: Double = 50
    55→}
    56→
    57→// MARK: - Gamification Thresholds
    58→
    59→enum GamificationThresholds {
    60→    /// Days for weekly streak badge
    61→    static let weeklyStreakDays = 7
    62→
    63→    /// Days for monthly streak badge
    64→    static let monthlyStreakDays = 30
    65→
    66→    /// Days for yearly streak badge
    67→    static let yearlyStreakDays = 365
    68→
    69→    /// Locations required for explorer badge
    70→    static let explorerLocations = 10
    71→
    72→    /// Distance in meters for marathoner badge (42km)
    73→    static let marathonerDistance: Double = 42000
    74→}
    75→
    76→// MARK: - UI Constants
    77→
    78→enum UIConstants {
    79→    /// Maximum pending tasks to show in dashboard
    80→    static let maxDashboardTasks = 3
    81→
    82→    /// Minimum height for expandable sections
    83→    static let minSectionHeight: CGFloat = 100
    84→
    85→    /// Maximum height for expandable sections
    86→    static let maxSectionHeight: CGFloat = 200
    87→}
    88→
    89→// MARK: - Animation Durations
    90→
    91→enum AnimationDurations {
    92→    /// Fast animations (button taps, small transitions)
    93→    static let fast: Double = 0.3
    94→
    95→    /// Normal animations (standard transitions)
    96→    static let normal: Double = 0.5
    97→
    98→    /// Slow animations (emphasis, larger transitions)
    99→    static let slow: Double = 0.8
   100→
   101→    /// Very slow animations (dramatic reveals)
   102→    static let slowest: Double = 1.5
   103→
   104→    /// XP toast display duration
   105→    static let xpToastDuration: Double = 3.0
   106→
   107→    /// XP floating animation duration
   108→    static let xpFloatDuration: Double = 2.0
   109→}
   110→
   111→// MARK: - Geofence Constants
   112→
   113→enum GeofenceConstants {
   114→    /// Maximum geofences allowed by iOS
   115→    static let maxGeofences = 20
   116→
   117→    /// Default geofence radius in meters
   118→    static let defaultRadius: Double = 100
   119→}
   120→
   121→// MARK: - API Constants
   122→
   123→enum APIConstants {
   124→    /// Current Anthropic API version (validated Jan 2026)
   125→    static let anthropicVersion = "2023-06-01"
   126→
   127→    /// Default model for Claude API
   128→    static let defaultModel = "claude-sonnet-4-20250514"
   129→
   130→    /// Maximum tokens for API response
   131→    static let maxTokens = 4096
   132→}
   133→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  Constants.swift
//  Planner2026-iOS
//
//  App-wide constants to avoid magic numbers
//

import Foundation

// MARK: - Data Retention Limits

enum DataLimits {
    /// Maximum location history entries to keep
    static let maxLocationHistory = 1000

    /// Maximum AI conversations to retain
    static let maxConversations = 50

    /// Maximum AI insights to retain
    static let maxInsights = 100

    /// Maximum recent XP events to display
    static let maxRecentXPEvents = 50

    /// Days to retain daily schedules
    static let schedulesRetentionDays = 30

    /// Days to retain morning ritual history
    static let morningRitualsRetentionDays = 90

    /// Days to retain daily routine history
    static let dailyRoutinesRetentionDays = 90
}

// MARK: - Walking Quest Thresholds

enum WalkingThresholds {
    /// Minimum minutes for short walk bonus
    static let shortWalkMinutes = 15

    /// Minimum minutes for long walk bonus
    static let longWalkMinutes = 30

    /// Default target minutes per day
    static let defaultTargetMinutes = 30

    /// Distance threshold for "nearby" locations in meters
    static let nearbyRadius: Double = 500

    /// Distance threshold for "explored" location in meters
    static let exploredRadius: Double = 50

    /// Minimum location accuracy in meters
    static let minLocationAccuracy: Double = 50
}

// MARK: - Gamification Thresholds

enum GamificationThresholds {
    /// Days for weekly streak badge
    static let weeklyStreakDays = 7

    /// Days for monthly streak badge
    static let monthlyStreakDays = 30

    /// Days for yearly streak badge
    static let yearlyStreakDays = 365

    /// Locations required for explorer badge
    static let explorerLocations = 10

    /// Distance in meters for marathoner badge (42km)
    static let marathonerDistance: Double = 42000
}

// MARK: - UI Constants

enum UIConstants {
    /// Maximum pending tasks to show in dashboard
    static let maxDashboardTasks = 3

    /// Minimum height for expandable sections
    static let minSectionHeight: CGFloat = 100

    /// Maximum height for expandable sections
    static let maxSectionHeight: CGFloat = 200
}

// MARK: - Animation Durations

enum AnimationDurations {
    /// Fast animations (button taps, small transitions)
    static let fast: Double = 0.3

    /// Normal animations (standard transitions)
    static let normal: Double = 0.5

    /// Slow animations (emphasis, larger transitions)
    static let slow: Double = 0.8

    /// Very slow animations (dramatic reveals)
    static let slowest: Double = 1.5

    /// XP toast display duration
    static let xpToastDuration: Double = 3.0

    /// XP floating animation duration
    static let xpFloatDuration: Double = 2.0
}

// MARK: - Geofence Constants

enum GeofenceConstants {
    /// Maximum geofences allowed by iOS
    static let maxGeofences = 20

    /// Default geofence radius in meters
    static let defaultRadius: Double = 100
}

// MARK: - API Constants

enum APIConstants {
    /// Current Anthropic API version (validated Jan 2026)
    static let anthropicVersion = "2023-06-01"

    /// Default model for Claude API
    static let defaultModel = "claude-sonnet-4-20250514"

    /// Maximum tokens for API response
    static let maxTokens = 4096
}


      56 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Utilities/XPCategoryHelper.swift
      78 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Utilities/Logger.swift
     132 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Utilities/Constants.swift
     132 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Utilities/HapticManager.swift
     143 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Auth/KeychainService.swift
     158 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Location/LocationService.swift
     191 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Auth/AuthenticationService.swift
     205 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Location/GeofenceService.swift
     227 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/AI/ClaudeService.swift
     264 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Utilities/NotificationService.swift
     278 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Location/WalkingTrackingService.swift
     347 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Data/GamificationService.swift
     532 /Users/neog/Apps/iOS/PROD-Planner2026/Planner2026-iOS/Services/Data/DataManager.swift
    2743 total


Perfect! Now let me create a comprehensive analysis document. Let me read one more authentication file for completeness:


     1→//
     2→//  KeychainService.swift
     3→//  Planner2026-iOS
     4→//
     5→//  Secure credential storage using iOS Keychain
     6→//
     7→
     8→import Foundation
     9→import Security
    10→
    11→final class KeychainService {
    12→    static let shared = KeychainService()
    13→
    14→    // MARK: - Configuration
    15→
    16→    private let serviceName = "com.neog.Planner2026-iOS"
    17→
    18→    // MARK: - Credential Keys
    19→
    20→    private enum CredentialKey: String, CaseIterable {
    21→        case appleUserID = "appleUserID"
    22→        case appleUserEmail = "appleUserEmail"
    23→        case appleUserName = "appleUserName"
    24→        case claudeAPIKey = "claudeAPIKey"
    25→    }
    26→
    27→    private init() {}
    28→
    29→    // MARK: - Apple Sign-In Credentials
    30→
    31→    var appleUserID: String? {
    32→        get { get(forKey: CredentialKey.appleUserID.rawValue) }
    33→        set {
    34→            if let value = newValue {
    35→                set(value, forKey: CredentialKey.appleUserID.rawValue)
    36→            } else {
    37→                remove(forKey: CredentialKey.appleUserID.rawValue)
    38→            }
    39→        }
    40→    }
    41→
    42→    var appleUserEmail: String? {
    43→        get { get(forKey: CredentialKey.appleUserEmail.rawValue) }
    44→        set {
    45→            if let value = newValue {
    46→                set(value, forKey: CredentialKey.appleUserEmail.rawValue)
    47→            } else {
    48→                remove(forKey: CredentialKey.appleUserEmail.rawValue)
    49→            }
    50→        }
    51→    }
    52→
    53→    var appleUserName: String? {
    54→        get { get(forKey: CredentialKey.appleUserName.rawValue) }
    55→        set {
    56→            if let value = newValue {
    57→                set(value, forKey: CredentialKey.appleUserName.rawValue)
    58→            } else {
    59→                remove(forKey: CredentialKey.appleUserName.rawValue)
    60→            }
    61→        }
    62→    }
    63→
    64→    // MARK: - Claude API Key
    65→
    66→    var claudeAPIKey: String? {
    67→        get { get(forKey: CredentialKey.claudeAPIKey.rawValue) }
    68→        set {
    69→            if let value = newValue {
    70→                set(value, forKey: CredentialKey.claudeAPIKey.rawValue)
    71→            } else {
    72→                remove(forKey: CredentialKey.claudeAPIKey.rawValue)
    73→            }
    74→        }
    75→    }
    76→
    77→    /// Check if user is signed in with Apple
    78→    var isAppleSignedIn: Bool {
    79→        guard let userID = appleUserID else { return false }
    80→        return !userID.isEmpty
    81→    }
    82→
    83→    /// Clear Apple Sign-In credentials (for sign out)
    84→    func clearAppleCredentials() {
    85→        appleUserID = nil
    86→        appleUserEmail = nil
    87→        appleUserName = nil
    88→    }
    89→
    90→    /// Clear all credentials
    91→    func clearAll() {
    92→        for key in CredentialKey.allCases {
    93→            remove(forKey: key.rawValue)
    94→        }
    95→    }
    96→
    97→    // MARK: - Private Methods
    98→
    99→    private func set(_ value: String, forKey key: String) {
   100→        guard let data = value.data(using: .utf8) else { return }
   101→
   102→        let query: [String: Any] = [
   103→            kSecClass as String: kSecClassGenericPassword,
   104→            kSecAttrService as String: serviceName,
   105→            kSecAttrAccount as String: key
   106→        ]
   107→
   108→        // Delete existing item first
   109→        SecItemDelete(query as CFDictionary)
   110→
   111→        // Add new item
   112→        var newQuery = query
   113→        newQuery[kSecValueData as String] = data
   114→        newQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
   115→
   116→        SecItemAdd(newQuery as CFDictionary, nil)
   117→    }
   118→
   119→    private func get(forKey key: String) -> String? {
   120→        let query: [String: Any] = [
   121→            kSecClass as String: kSecClassGenericPassword,
   122→            kSecAttrService as String: serviceName,
   123→            kSecAttrAccount as String: key,
   124→            kSecReturnData as String: true,
   125→            kSecMatchLimit as String: kSecMatchLimitOne
   126→        ]
   127→
   128→        var result: AnyObject?
   129→        let status = SecItemCopyMatching(query as CFDictionary, &result)
   130→
   131→        guard status == errSecSuccess, let data = result as? Data else { return nil }
   132→        return String(data: data, encoding: .utf8)
   133→    }
   134→
   135→    private func remove(forKey key: String) {
   136→        let query: [String: Any] = [
   137→            kSecClass as String: kSecClassGenericPassword,
   138→            kSecAttrService as String: serviceName,
   139→            kSecAttrAccount as String: key
   140→        ]
   141→        SecItemDelete(query as CFDictionary)
   142→    }
   143→}
   144→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

//
//  KeychainService.swift
//  Planner2026-iOS
//
//  Secure credential storage using iOS Keychain
//

import Foundation
import Security

final class KeychainService {
    static let shared = KeychainService()

    // MARK: - Configuration

    private let serviceName = "com.neog.Planner2026-iOS"

    // MARK: - Credential Keys

    private enum CredentialKey: String, CaseIterable {
        case appleUserID = "appleUserID"
        case appleUserEmail = "appleUserEmail"
        case appleUserName = "appleUserName"
        case claudeAPIKey = "claudeAPIKey"
    }

    private init() {}

    // MARK: - Apple Sign-In Credentials

    var appleUserID: String? {
        get { get(forKey: CredentialKey.appleUserID.rawValue) }
        set {
            if let value = newValue {
                set(value, forKey: CredentialKey.appleUserID.rawValue)
            } else {
                remove(forKey: CredentialKey.appleUserID.rawValue)
            }
        }
    }

    var appleUserEmail: String? {
        get { get(forKey: CredentialKey.appleUserEmail.rawValue) }
        set {
            if let value = newValue {
                set(value, forKey: CredentialKey.appleUserEmail.rawValue)
            } else {
                remove(forKey: CredentialKey.appleUserEmail.rawValue)
            }
        }
    }

    var appleUserName: String? {
        get { get(forKey: CredentialKey.appleUserName.rawValue) }
        set {
            if let value = newValue {
                set(value, forKey: CredentialKey.appleUserName.rawValue)
            } else {
                remove(forKey: CredentialKey.appleUserName.rawValue)
            }
        }
    }

    // MARK: - Claude API Key

    var claudeAPIKey: String? {
        get { get(forKey: CredentialKey.claudeAPIKey.rawValue) }
        set {
            if let value = newValue {
                set(value, forKey: CredentialKey.claudeAPIKey.rawValue)
            } else {
                remove(forKey: CredentialKey.claudeAPIKey.rawValue)
            }
        }
    }

    /// Check if user is signed in with Apple
    var isAppleSignedIn: Bool {
        guard let userID = appleUserID else { return false }
        return !userID.isEmpty
    }

    /// Clear Apple Sign-In credentials (for sign out)
    func clearAppleCredentials() {
        appleUserID = nil
        appleUserEmail = nil
        appleUserName = nil
    }

    /// Clear all credentials
    func clearAll() {
        for key in CredentialKey.allCases {
            remove(forKey: key.rawValue)
        }
    }

    // MARK: - Private Methods

    private func set(_ value: String, forKey key: String) {
        guard let data = value.data(using: .utf8) else { return }

        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: key
        ]

        // Delete existing item first
        SecItemDelete(query as CFDictionary)

        // Add new item
        var newQuery = query
        newQuery[kSecValueData as String] = data
        newQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock

        SecItemAdd(newQuery as CFDictionary, nil)
    }

    private func get(forKey key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)

        guard status == errSecSuccess, let data = result as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }

    private func remove(forKey key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: key
        ]
        SecItemDelete(query as CFDictionary)
    }
}