Recurring tasks only ever showed on their single dueDate. Add a
recurrence engine that expands occurrences per-date with independent
per-occurrence completion.
- TaskItem: recurrenceEnd + completedOccurrences (optional, safe decode).
- TaskViewModel: isOccurrence/occurrenceComplete/toggleOccurrence/
occurrenceCopy/nextOccurrence/occurrences(on:); Daily/Weekly/Monthly/
Yearly/Every-Weekday; respects recurrenceEnd; works across years.
- toggle() routes recurring rows to per-occurrence completion.
- Date filters re-inject per-date occurrence copies; recurring excluded
from overdue. Calendar dots + day detail use occurrences(on:).
Also log KC-11 (year display) + KC-12.
Pending: "repeat until" date UI (engine already honors recurrenceEnd).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navigation was unbounded, but headers only showed the month name so the
year change was invisible and future months felt unreachable.
- Main calendar header now shows "Month Year".
- Date-picker grid label shows the year.
- Year view gets a chevron year navigator (unbounded) + year label.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The calendar marked every occurrence of a program complete once any one
was finished — including future dates — because DayTimelineView read the
program's live shared doneSets.
- Add WorkoutViewModel.isWorkoutComplete(on:) backed by workoutDates.
- DayTimelineView takes workoutDone and uses it instead of live sets;
Calendar passes the selected date's state, Today passes today's.
- A date now shows complete only if that specific date was finished.
Log KC-10 in ISSUES.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add per-day workout history: WorkoutDayLog/LoggedExercise/LoggedSet
models stored at kisani.workout.history.<uid> (iCloud-synced).
- snapshotDay(_:) captures the active program's completed sets on full
completion and before the daily reset; only days with >=1 done set.
- New WorkoutHistoryView (clock button in the Workout header) lists past
days newest-first with per-exercise breakdown.
- Log KC-8 (auto-switch / Rest Day) and KC-9 (history) in ISSUES.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Workout tab kept showing the last-used program (with its old
completion) regardless of the weekly schedule, so a rest day still
displayed a stale, fully-checked workout.
- On a new day, the active program now follows the weekly schedule
(applyScheduledProgramForToday in resetSetsForNewDayIfNeeded).
- Add isRestDayToday + a Rest Day card shown when today has no scheduled
program, with a "Work out anyway" override. Header reads "Rest Day".
- Daily set reset (KC-1) unchanged — no day starts pre-checked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
App Store upload rejected the bundle: iPad-multitasking apps must declare
a launch screen, but project.yml set an empty UILaunchStoryboardName ("")
and provided no UILaunchScreen.
Add a minimal LaunchScreen.storyboard (blank, systemBackgroundColor) and
point INFOPLIST_KEY_UILaunchStoryboardName at it. Regenerated the project.
Bundle now ships UILaunchStoryboardName=LaunchScreen + compiled
LaunchScreen.storyboardc; builds clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KC-5: Consolidate calendar access into CalendarStore.shared so Today,
Calendar, Onboarding, and Settings share one permission state and event
cache. requestAccess() debounces and reads authoritative status; turning
on "Show Calendar Events" while unauthorized now prompts. New Settings →
Data → Calendar row shows Connected/Connect/Denied.
KC-6: Add calendar-event notifications. CalendarStore.upcomingEventNotifs
snapshots visible events (next 7 days); NotificationManager schedules at
start time plus each event alarm (all-day at 9am), capped at ~20 to stay
under iOS's 64-notification limit. Wired into reschedule().
Log KC-5 and KC-6 in ISSUES.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce CalendarStore.shared so Today, Calendar, and Onboarding read
the same auth status and event cache instead of each spinning up its own
EKEventStore. Trust the system's authoritative authorization status over
the granted bool, debounce repeat permission requests, route denied users
to Settings, and prompt when enabling local calendars without access.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task context menu (new shared TaskMenuItems) consistent across Today,
Matrix, and Calendar: Pin · Date · Move · Priority · Tags · Add to Live
Activity · Delete. Adds TaskViewModel.setPriority.
Live Activity (ActivityKit):
- TaskActivityAttributes shared between app and widget targets
- LiveActivityManager (start/update/end/toggle, iOS 16.1+)
- TaskLiveActivity widget: lock-screen banner + Dynamic Island
- NSSupportsLiveActivities via project.yml; project regenerated
Calendar events (non-subscription, writable only):
- Edit via system EKEventEditViewController (EventEditView wrapper)
- Delete via new CalendarStore.deleteEvent
Also sweeps in prior in-progress edits already present in the working
tree (AuthManager, NotificationManager, TaskItem, task views) and the
updated ISSUES.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move the Overdue card to sit directly below today's active tasks
(above Next 3 Days) so your real tasks stay at the top.
- Filter subscription/RSS calendar feeds (F1, TV shows, etc.) out of the
Today/Tasks timeline; they remain in the Calendar tab. Birthdays and
personal calendars are unaffected.
Note: also carries prior in-progress TodayView edits that were already
in the working tree before this session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KC-1: Set completion was stored permanently on the program, so the same
program scheduled on back-to-back days (e.g. Shoulders Mon + Tue) kept
yesterday's checkmarks. Add resetSetsForNewDayIfNeeded() to clear every
set's isDone when the calendar day changes (cold launch, onAppear, and
scenePhase active). Streak history is untouched.
KC-2: Replace all 12 AppIcon sizes with the new KisaniCal mark, alpha
flattened onto RGB(26,29,34) so the 1024 App Store icon is fully opaque.
Add KisaniCal/ISSUES.md tracker (KC-1, KC-2: In Progress).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- My Tasks widget: today's tasks with interactive check-off (ToggleTaskIntent)
and a "+" that opens the app's add-task sheet; writes preserve all task
fields and sync via the App Group.
- Event Countdown: AZURE/AWS-style header (accent name + countdown label +
percent); tap the label to cycle days → weeks → time remaining.
- Today: collapsible "Completed" archive section below Upcoming.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Widgets (new KisaniCalWidgets extension)
- Event Countdown widget: configurable via AppIntent, pick from your events
or set a custom name/date; dot-grid progress toward the event.
- Day Progress and Tasks-Done-Today widgets; lock screen widgets.
- Shared dot grid sizes circles to fill the widget evenly at any count.
- Tasks/calendar prefs now persist to the App Group so widgets read live data.
Health & streaks
- Persist HealthKit authorization and re-establish on launch (fixes the
Today dashboard and streak sync disappearing after relaunch).
- Add a Health permission toggle to onboarding; unify Settings/Profile on the
manager's state.
- Day streak counts from a logged Health workout OR marking all exercises
complete; nudge to mark exercises complete even when Health logged the workout.
Workout editor
- Drag to reorder exercises and move them across sections (drag-and-drop);
swipe to delete.
- Add-exercise sheet: global search and keep-adding-multiple with a running count.
Fixes
- Restore app entitlements link (Sign in with Apple, HealthKit, App Group, iCloud).
- Add HealthKit usage strings; widget Info.plist NSExtension + nested bundle id.
- Calendar "Connect" routes to Settings when access was denied.
- Bake DEVELOPMENT_TEAM and shared versions into project.yml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- HealthKitManager: add fetchWorkoutDates(from:to:) to read past workouts from Apple Health
- WorkoutViewModel: syncFromHealthKit() merges HealthKit workout dates into streak (runs on every foreground); checkPendingHealthConfirm() picks up confirmed workouts from notification action
- NotificationManager: WORKOUT_CONFIRM category with 'Yes, log it' action; scheduleWorkoutConfirm() fires weekly on scheduled workout days at configurable time (default 8pm); LOG_WORKOUT action writes pendingConfirm to UserDefaults
- SettingsView (Workout Settings): new HEALTH SYNC section with 'Sync now' button and 'Workout confirmation reminder' toggle + time picker
- TodayView: TodayHealthStrip shows steps, active calories, resting HR, and workout streak in a 4-pill row below the date header (only shown when HealthKit is authorized); refreshes on appear and foreground
- FAB: Matrix FAB padding fixed to match Today/Calendar position (.bottom 10)
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
- Add NSUbiquitousKeyValueStore entitlement to KisaniCal.entitlements
- New CloudSyncManager: mirrors UserDefaults to iCloud KV Store on every save; restores to UserDefaults on fresh install when local data is missing; observes external changes to pull updates in real-time; guards against 1 MB limit
- TaskViewModel: restore from iCloud on init, push to iCloud on every save
- WorkoutViewModel: restore programs/schedule/activePid/completedDates from iCloud on init, push all four keys on every save
- ContentView: restoreSettings() + backupSettings() on appear and foreground — syncs ~18 small settings keys (appearance, tabs, tutorial flags, calendar prefs, workout notifications)
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
- Add first-visit tutorial cards for TodayView (3 steps) and WorkoutView (3 steps) using new InViewTutorialCard component; each has independent AppStorage completion flag
- Fix FAB hidden behind custom tab bar in MatrixView by moving NavigationStack inside outer ZStack so FAB sits at the correct safe area level
- Fix task colors throughout app to use quadrant.color/softColor instead of taskColor — tasks now consistently show orange/yellow/blue/green matching their matrix quadrant in Today timeline, calendar dots, and day view
- Fix WorkoutView background (move ignoresSafeArea out of ZStack children into .background modifier)
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
foregroundStyle is overridden by Menu tint; baking color into UIImage
with .alwaysOriginal rendering mode is the only reliable approach.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Priority stored on TaskItem, shown as colored flag in add sheet toolbar
and as a small flag indicator on each task row.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Tap body → marks complete + navigates to tasks.
Swipe action 'Mark Complete' → marks complete without opening app.
TaskViewModel.reload() called on foreground to sync UserDefaults changes
made while app was backgrounded.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Single-size universal entry was not generating 20pt notification sizes.
Added all traditional sizes (20/29/40/60/76/83.5pt) so iOS notification
center finds the pre-baked icon directly.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Wrapping the entire TaskRowView in a Button caused any tap on the row
to mark the task complete. Circle is now the only interactive element.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Evening check-in no longer embeds the urgent task count in the body —
repeating notifications bake that value at schedule time so it always
shows a stale number. Replaced with a generic prompt.
didReceive now routes notification taps via ShortcutHandler:
'workout' deeplink → Workout tab, everything else → Today tab.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
updateBadge() counts all incomplete tasks with a due date earlier
than tomorrow (i.e. overdue or due today) and calls setBadgeCount().
Called inside reschedule() so the badge updates on foreground, task
changes, and workout set completions — same triggers as notifications.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
- TutorialManager: tracks 4 steps (NLP, Calendar, Matrix, Workout)
persisted via AppStorage; shown once then never again
- TutorialView: fullscreen overlay with even-odd spotlight cutout,
animated accent border, ultraThinMaterial callout card, pill progress
dots, Next/Get started button, and Skip
- TutorialFrameKey: PreferenceKey collects CGRect for each anchor index;
.tutorialAnchor() modifier wired to FAB (step 0) and all 4 tab buttons
- Callout auto-positions above or below the spotlight based on screen half
- ContentView collects frames via onPreferenceChange and layers the overlay
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Needed because self is captured in @Sendable closures passed to
UNUserNotificationCenter callbacks. Safe: the only mutable state
(isAuthorized) is exclusively written on the main thread.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
- Remove @MainActor from class (caused all Sendable/isolation cascade)
- Keep @MainActor only on reschedule() where VM access is needed
- Snapshot tasks in reschedule() so scheduleTasks() never captures
a non-Sendable TaskViewModel across async boundaries
- Capture center locally in scheduleTasks() to avoid @MainActor
property access inside the @Sendable getPendingNotificationRequests callback
- Mark UNUserNotificationCenterDelegate methods nonisolated to satisfy
protocol conformance without actor crossing
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Snapshot WorkoutViewModel's schedule/programs/progress before the
async getNotificationSettings callback so scheduleWorkout receives
plain value types instead of MainActor-isolated properties.
Also fix unused-mutation warning: email in handleApple changed to let.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Profile header and ProfileStatsSheet now read displayName and email
from AuthManager.shared instead of hardcoded "Robin Kutesa" / "RK".
Initials are computed dynamically from the user's display name.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
- Remove leftover Self. prefix on instance storage keys in save()
- Mark TaskViewModel and WorkoutViewModel @MainActor so init() can
safely access AuthManager.shared without isolation violations
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
- Remove all personal sampleTasks from TaskViewModel (tasks start empty)
- Namespace UserDefaults keys by Apple ID for task and workout data isolation
- Add ProfileSetupView for name capture after first Sign in with Apple
- Add updateDisplayName() to AuthManager
- Route to ProfileSetupView when user has no display name yet
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
- Tab bar: TickTick-style floating pill (cornerRadius 28, shadow, systemGray5 active highlight)
- NLP: month+day parsing (8th June, June 8), recurrence (every year/week/etc), intent stripping (remind me of)
- Recurrence chip in add-task toolbar; isRecurring saved with task
- Date chip defaults to Today on new tasks; tapping opens full date picker
- Date picker: calendar grid, inline time wheel, Repeat menu (Date tab)
- Duration tab: Start/End column selector, live duration counter, All Day toggle
- TaskItem: added endDate and isAllDay fields
- Fixed hidden Unicode character corrupting + operator in TodayView
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Default view shows the next 5 upcoming tasks. If more exist, a
"N more ↓" button expands the full list; tapping again collapses
back to 5. The section header is tappable to fully hide the list
(only title + count badge remain visible). Collapsed state is
persisted via @AppStorage so it survives app restarts.
Adds four UIApplicationShortcutItems registered dynamically at launch:
• Add Task — opens the add-task sheet immediately, any tab
• Today — switches to the Today tab
• Calendar — switches to the Calendar tab
• Workout — switches to the Workout tab (only registered if the
workout tab is enabled in settings; re-registers when the setting changes)
Cold-launch and foreground-activation paths both handled via AppDelegate.
ShortcutHandler singleton bridges UIKit callbacks to SwiftUI via @Published.
Selected-day panel below the month grid now renders as a proper vertical
timeline: time labels in the left gutter, continuous track line with
circle markers, and white cards for each item. All-day tasks and events
get a stroke circle; timed entries get a solid colored dot; workout gets
a dumbbell icon; completed items show a checkmark circle with strikethrough.
Tasks can be toggled directly from the timeline card. Cal events show
end time as subtitle. Empty state shows a calendar icon.
DotGridProgress replaced GeometryReader + 200+ Circle view nodes with a
single Canvas draw call. Canvas receives its own size parameter so no
GeometryReader is needed; SwiftUI interpolates the progress value through
the .animation modifier for smooth fill transitions.
FloatingTabState ScrollYKey made private to TabBarScrollModifier to limit
its PreferenceKey scope and prevent unintended propagation across unrelated
subtrees.
Replace ProgressRingCard in WorkoutView with DotProgressCard using the
same dot grid as the workout session sheet. Remove private from DotRow
so it's accessible across view files.
iOS auto-snaps to the next larger detent when keyboard appears over a
text field. Removing .large locks the sheet to .height(150) so it
stays compact and hugs the keyboard like the reference app.
Also removed the Description label and reduced input maxHeight to 72pt
so all content fits within the 150pt detent.
Use UIDevice.userInterfaceIdiom instead of hSizeClass so large iPhones
in landscape keep the iPhone layout. Move tabState environmentObject to
the top-level Group so both layouts always have it in scope.