# KisaniCal — Issue Tracker Status legend: `Open` · `In Progress` · `Resolved` --- ## KC-1 — Daily workout log does not reset on a new day **Status:** In Progress (fix implemented & tested locally, pending release) **Reported by:** User **Area:** Workout ### Description When the same workout program is scheduled on back-to-back days (e.g. Shoulders on Monday **and** Tuesday), the new day does not start fresh. Completed sets from the previous day stay checked, so the workout appears already done. ### Root cause Set completion (`ExerciseSet.isDone`) was stored permanently inside the `WorkoutProgram` and persisted via `save()`. There was no per-day reset, so a program reused the next day carried over yesterday's checkmarks. ### Fix Set completion is now a per-day state: - `WorkoutViewModel.resetSetsForNewDayIfNeeded()` clears every set's `isDone` across all programs when the calendar day changes (skips first launch, no-ops when the day is unchanged). - `toggleSet(...)` stamps the active day so on-screen state always belongs to today. - Called on cold launch (`init`), `.onAppear`, and `scenePhase == .active` (handles crossing midnight while the app is alive). - Streak history (`workoutDates`) is untouched, so completed days still count. Files: `KisaniCal/Models/ExerciseModels.swift`, `KisaniCal/ContentView.swift` ### Verification Builds cleanly (Xcode 26.2 SDK, simulator). Verified in-process on the simulator via the real `resetSetsForNewDayIfNeeded()` against a seeded completed set: `before=[true] after=[false] day=2026-06-06 -> PASS`. Streak preserved. Committed in `cdc46e9`. --- ## KC-2 — App logo update (new KisaniCal brand mark) **Status:** In Progress (assets installed & build-verified, pending release) **Area:** Branding / App Icon ### Description Replace the app icon with the new KisaniCal logo (orange dot grid on dark) across the App Store marketing icon and all home/lock screen / Spotlight / notification sizes. ### Fix All 12 sizes in `AppIcon.appiconset` regenerated from the new source. Alpha flattened onto the brand dark background `RGB(26,29,34)` so the 1024 App Store icon is fully opaque (App Store Connect compliant) and device icons are full-bleed. Files: `KisaniCal/Assets.xcassets/AppIcon.appiconset/` ### Verification Builds cleanly (app + widget extension); `hasAlpha: no` on all icons. New logo confirmed compiled into `Assets.car` (home, lock-screen notification, Spotlight, Settings all derive from `AppIcon`). Committed in `cdc46e9`. Note: device still shows old icon until the new build is installed (delete + reinstall). --- ## KC-3 — Today view: section order + RSS feeds leaking into Tasks **Status:** In Progress (implemented & build-verified, pending release) **Reported by:** User **Area:** Tasks / Today ### Description Two changes to the Today view: 1. The Overdue card sat at the very top, above today's active tasks. Active tasks should always be the top section. 2. Subscribed/RSS calendar feeds (F1 schedule, TV-show calendars) were appearing in the Today/Tasks timeline. They belong in the Calendar tab only. ### Fix - Moved the `OverdueCard` to render directly below today's active tasks (above Next 3 Days), so active tasks stay at the top. - `todayCalEvents` now excludes events whose `calendar.type == .subscription`, so RSS/ICS feeds are filtered out of the Today timeline. The Calendar view reads `monthEvents` directly and is unaffected. - Birthdays (`.birthday`) and personal/CalDAV calendars are intentionally left visible; the existing per-calendar visibility toggle (`hiddenCalendarIDs`) lets the user hide any of them. User-created tasks are never affected. Files: `KisaniCal/Views/TodayView.swift` ### Verification Builds cleanly. Committed in `f66f032` (re-authored `a92e9d4`). --- ## KC-4 — Unified task context menu + Live Activity + calendar event actions **Status:** In Progress (implemented & build-verified; Live Activity pending on-device test) **Reported by:** User **Area:** Tasks / Calendar / Widgets ### Description Long-pressing a task (in any task list) should show a consistent menu: Pin · Date · Move · Priority · Tags · Add to Live Activity · Delete — and the actions must actually work. Long-pressing a non-subscription calendar event should offer Edit / Delete only. ### Fix - New shared `TaskMenuItems` (KisaniCal/Views/TaskContextMenu.swift) used by the Today timeline, Matrix (grid + Overdue/Later/Completed lists), and Calendar day timeline. Added `TaskViewModel.setPriority`. - Live Activity (ActivityKit): `TaskActivityAttributes` (shared app+widget), `LiveActivityManager` (start/update/end/toggle, iOS 16.1+), `TaskLiveActivity` widget (lock screen + Dynamic Island), `NSSupportsLiveActivities` via project.yml. - Calendar events (non-subscription, writable only): Edit opens the system `EKEventEditViewController` (`EventEditView`); Delete via `CalendarStore.deleteEvent`. Files: `TaskContextMenu.swift`, `TaskActivityAttributes.swift`, `LiveActivityManager.swift`, `TaskLiveActivity.swift`, `TaskItem.swift`, `MatrixView.swift`, `CalendarView.swift`, `TodayView.swift`, `KisaniCalWidgets.swift`, `project.yml`. Committed in `d0d982f`. ### How to test 1. **Menu parity** — long-press a task in Today, in a Matrix quadrant, in a Matrix list (Overdue/Later), and in the Calendar day view. All show the same 7 items. 2. **Each action** — Pin (row pins), Date → Today/Tomorrow/Next Week/Remove (due date changes), Move (task changes quadrant), Priority (flag changes), Tags (category changes), Delete (removed). 3. **Live Activity** — tap "Add to Live Activity" → a banner appears on the Lock Screen / Dynamic Island with the task title + countdown; tap again to end. **Requires a real iOS 16.1+ device** (not visible in the Simulator). 4. **Calendar events** — long-press a personal (writable) event → Edit opens the system editor; Delete removes it. A subscribed/RSS event (F1, TV) shows **no** menu. ### Known limitations - Live Activity visuals only render on a physical device; the design is a first pass (orange accent + timer) and can be refined. - Uses the iOS 16.1 `Activity.request(...contentState:...)` API (deprecation warning on 16.2+, still functional). --- ## KC-5 — Calendar "Connect" toggle unreliable / inconsistent across screens **Status:** In Progress (implemented & build-verified; full reliability win is device-only) **Reported by:** User **Area:** Calendar / Permissions ### Description The iPhone Calendar "Connect" sometimes needed repeated taps, onboarding sometimes didn't reflect a granted calendar, and there was no place to see/manage connection status from Settings. ### Root cause Three independent calendar-permission states that never synced: `TodayView`, `CalendarView` each had their own `CalendarStore` + `EKEventStore`, and `OnboardingView` had its own raw `EKEventStore`. Granting in one left the others showing stale cached status until they happened to re-read it. ### Fix - `CalendarStore.shared` singleton — one source of truth used by Today, Calendar, Onboarding, and Settings. Connect once, every screen reflects it. - `requestAccess()` now debounces in-flight requests and reads the **authoritative** `EKEventStore.authorizationStatus` instead of trusting the callback bool; already-decided states re-sync via `refreshStatus()`. - Turning on "Show Calendar Events" while unauthorized now triggers the prompt. - New **Settings → Data → Calendar** row showing ● Connected / Connect / Denied, opening the connect sheet. Files: `CalendarView.swift`, `TodayView.swift`, `OnboardingView.swift`, `SettingsView.swift`. ### Verification Builds cleanly. Cross-screen consistency + the Settings status row are testable in the Simulator; the request-reliability fix is best confirmed on a device. --- ## KC-6 — No notifications for calendar events **Status:** In Progress (implemented & build-verified; fires on a real device with calendar access) **Reported by:** User **Area:** Calendar / Notifications ### Description Task reminders fired, but calendar events never produced a KisaniCal notification — events with no iOS alert (e.g. subscribed F1/TV feeds) notified the user of nothing. ### Root cause `NotificationManager` only scheduled workout + per-task notifications. There was no calendar-event path at all (`EKEvent`/`CalendarStore` were never involved). ### Fix - `CalendarStore.upcomingEventNotifs(days:)` snapshots visible events for the next 7 days (respects enabled flag + hidden calendars) with start time + alarm offsets. - `NotificationManager.scheduleCalendarEvents(...)` schedules a notification at the event start ("on time") plus at each alarm the user set; all-day events fire at 9am morning-of. Capped at ~20 to stay under iOS's 64 pending-notification limit; recurring instances kept unique via start-time in the id. - Wired into `reschedule(...)` (runs on foreground / scene-active / task changes). Files: `CalendarView.swift`, `NotificationManager.swift`. ### How to test (device, calendar access granted) 1. Create an event a few minutes out with **no alert** on a visible calendar. 2. Background the app (triggers a reschedule). 3. Notification fires at the event's start time. ### Known limitations / decisions - "All visible events" was chosen, so personal events that already have an iOS alert may double (system + KisaniCal). Feed events (F1/TV) are the main win. - Tapping a calendar notification opens the Today view (no calendar deeplink). - The 7-day window re-scans on each foreground; no background refresh beyond that. --- ## KC-7 — App Store upload: "iPad Multitasking must provide a launch screen" **Status:** Resolved (fix in `72f4566`; re-archive required to ship) **Reported by:** User (App Store / Transporter validation) **Area:** Build / App Store ### Symptom Upload fails validation: > Invalid bundle. Apps that support Multitasking on iPad must provide the app's > launch screen using an Xcode storyboard, or using UILaunchScreen … Verify that > the UILaunchStoryboardName key is included in your com.kutesir.KisaniCal bundle. ### Root cause The app targets iPad (`TARGETED_DEVICE_FAMILY = "1,2"`) with multitasking (`UIApplicationSupportsMultipleScenes = true`), but `project.yml` had `INFOPLIST_KEY_UILaunchStoryboardName: ""` (empty) and no `UILaunchScreen` — so the bundle shipped no valid launch screen. ### Fix - Added `KisaniCal/LaunchScreen.storyboard` (blank, `systemBackgroundColor`). - `project.yml`: `INFOPLIST_KEY_UILaunchStoryboardName: LaunchScreen`. - `xcodegen generate`. Verified a clean **Release** build emits `UILaunchStoryboardName = LaunchScreen` and ships `LaunchScreen.storyboardc`. ### ⚡ If it happens again — solve it fast This error almost always recurs because a **stale archive** was uploaded, NOT because the code is wrong. Checklist: 1. Confirm the fix is present: `grep UILaunchStoryboardName project.yml` → must be `LaunchScreen` (not `""`). `git log --oneline -- KisaniCal/LaunchScreen.storyboard` → should show `72f4566`. 2. If Xcode was open, **reload the project** (xcodegen rewrites `.xcodeproj`). 3. **Product → Clean Build Folder (⇧⌘K)** — a cached build folder re-emits the old Info.plist. This is the step people skip. 4. **Bump `CURRENT_PROJECT_VERSION`** in `project.yml` + `xcodegen generate` (a failed upload can "use up" the build number). 5. Re-Archive → Validate → Distribute. 6. Sanity check any archive: `/usr/libexec/PlistBuddy -c "Print :UILaunchStoryboardName" .app/Info.plist` and `ls .app | grep LaunchScreen` (expect `LaunchScreen.storyboardc`). Committed: fix `72f4566`, build-number bump `345415e`. --- ## KC-8 — Workout doesn't follow the schedule; rest day shows a stale workout **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Workout ### Description The Workout tab kept showing the last-used program with its old completion, ignoring the weekly schedule. On a rest day (no program assigned) it still displayed a fully-checked workout from a previous day. ### Root cause The daily reset (KC-1) cleared set completion but never changed the **active program** — it stayed on whatever was last selected, regardless of the schedule. ### Fix - `applyScheduledProgramForToday()` (run from `resetSetsForNewDayIfNeeded` on a new day) switches the active program to the one assigned to today. - `isRestDayToday` (schedule exists but today has no entry) drives a new **Rest Day** card in `WorkoutView` with a "Work out anyway" override; header reads "Rest Day". - Daily set reset (KC-1) unchanged. Files: `ExerciseModels.swift`, `WorkoutView.swift`. Committed in `f9081cf`. --- ## KC-9 — Per-day workout history **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User ("store the data for each day") **Area:** Workout ### Description Set completion resets daily, so there was no way to look back at what was actually logged on a past day. ### Fix - New `WorkoutDayLog` / `LoggedExercise` / `LoggedSet` models; stored as `kisani.workout.history.` (and iCloud-synced). - `snapshotDay(_:)` captures the active program's completed sets — called from `logWorkoutCompleted` (same-day) and from the daily reset **before** wiping (captures partial days). Only records days with ≥1 set done. - New `WorkoutHistoryView` (clock button in the Workout header) lists past days newest-first with program, sets done, and per-exercise breakdown. Files: `ExerciseModels.swift`, `WorkoutView.swift`. --- ## KC-10 — Calendar marks future workouts complete (per-type, not per-date) **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Calendar / Workout ### Description In the calendar, completing one occurrence of a program (e.g. Tuesday Shoulders) showed **every** occurrence of that program as completed — including **future** days (Saturday Shoulders) — with a checkmark + strikethrough. ### Root cause `DayTimelineView` computed the workout's completion from the program's **live** shared set state: `w.doneSets == w.totalSets`. `program(for: date)` returns the same program object for every date it's scheduled, so its in-progress completion leaked onto all of those dates. ### Fix - Added `WorkoutViewModel.isWorkoutComplete(on:)` — checks `workoutDates` (the set of dates a workout was actually finished/Health-detected), i.e. **per-date**. - `DayTimelineView` now takes a `workoutDone: Bool` and uses it instead of the live `doneSets`. Calendar passes `isWorkoutComplete(on: selectedDate)`, Today passes `isWorkoutComplete(on: today)`. - Result: a date shows the workout complete only if that **specific date** was finished. Future occurrences are never auto-completed. Files: `ExerciseModels.swift`, `CalendarView.swift`, `TodayView.swift`. --- ## KC-11 — Calendar appeared capped at 2026 (no year shown) **Status:** Resolved **Reported by:** User **Area:** Calendar ### Description Users couldn't navigate/see dates beyond 2026. ### Root cause Navigation was actually unbounded, but every header showed only the month name (`"MMMM"`) — the Year view had no year label at all — so crossing a year boundary was invisible and future months felt unreachable (compounded by empty future months from the missing recurrence engine, KC-12). ### Fix Show the year: main header `"Month Year"`, date-picker grid `"MMMM yyyy"`, and a `◀ year ▶` navigator + label in the Year view (`navigateYear`). Files: `CalendarView.swift`, `TodayView.swift`. Committed in `7b2b61a`. --- ## KC-12 — Recurring tasks don't appear on future dates **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Tasks / Calendar ### Description A task set to repeat (Daily/Weekly/Monthly/Yearly/Every-Weekday) only ever showed on its single original `dueDate`. No future occurrences, no per-occurrence completion. ### Root cause There was no recurrence engine — tasks carry `isRecurring` + `recurrenceLabel` but a single `dueDate`, and every surface matched by `isDate(dueDate, inSameDayAs:)`. ### Fix (core) - Model: `recurrenceEnd` + `completedOccurrences` (optional → safe decode of old data). - `TaskViewModel` engine: `isOccurrence(_:on:)`, `occurrenceComplete`, `toggleOccurrence`, `occurrenceCopy`, `nextOccurrence`, `occurrences(on:)`. - `toggle(_:)` routes recurring rows to per-occurrence completion (one day never affects another). - Date filters (`today`/`next3`/`upcoming`/`completedToday`) exclude recurring masters and re-inject per-date occurrence copies; recurring never goes "overdue". - Calendar dots + day detail use `occurrences(on:)`. - Works across year boundaries; respects `recurrenceEnd` when set. Files: `TaskItem.swift`, `CalendarView.swift`. ### "Repeat until" UI (done) - Added `recurrenceEnd` through `NLParsed`, `TaskDatePickerSheet`, both add/edit sheets, and `addTask`/`updateTask`. - Date picker shows a "Repeat until" row (graphical date picker, default "Forever", with Clear) whenever a recurrence is selected. Files: `TaskItem.swift`, `CalendarView.swift`, `TodayView.swift`, `MatrixView.swift`. --- ## KC-13 — Recurrence redesign: single occurrence, new sections, classic Matrix **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Tasks / Calendar / Matrix ### Description The first recurrence pass used calendar-expansion in lists — a daily task showed in Today, Tomorrow, Next 7 Days, and Upcoming simultaneously (planning noise). Should behave like TickTick: one task, one next occurrence, one source of truth. ### Fix - **Single active occurrence (P1/P5):** `nextActiveOccurrence` + `activeRows` — each recurring task contributes ONE row (its next un-completed occurrence). Completing rolls forward; no future duplicates in lists. Calendar grid still shows per-date. - **Sections (P2):** replaced Today / Next 3 Days / Upcoming with **Today / Tomorrow / Next 7 Days / Later** (`todayTasks`/`tomorrowTasks`/ `next7DaysTasks`/`laterTasks`; `UpcomingSection` gained a `title`). - **Icons (P3):** ⏰ reminder + 🔁 recurring on the right of each row; fixed the hardcoded "Annual" label to show the real frequency. - **Matrix (P4):** classic Eisenhower — importance is user-assigned (row), urgency is derived from the deadline (within `urgentWindowDays` = 7). `displayQuadrant` + `matrixTasks` group by the computed quadrant; recurring rolls forward. Chosen over the fully-auto "far = Q4" model so the user keeps control of importance. Files: `TaskItem.swift`, `TodayView.swift`, `MatrixView.swift`. Commits: `b4e36db` (lists + icons), `2f1bc5d` (Matrix). --- ## KC-14 — Workout Stats (daily / weekly / monthly performance) **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Workout ### Description Per-day logs (KC-9) existed but weren't aggregated, so there was no way to compare daily/weekly/monthly performance. ### Fix - `WorkoutDayLog.volume` (Σ weight×reps over done sets); `StatPeriod`/`WorkoutStat`/ `StatBucket`. - `WorkoutViewModel`: `stat(period, offset:)` (current vs previous), `statInterval`, `statBuckets(period, count:)` for charting. - New `WorkoutStatsView` (chart-bar button in the Workout header): Day/Week/Month selector, summary cards (Workouts / Sets / Volume) with %-delta vs the previous period, a metric chooser, and a Swift Charts bar trend (last 14 days / 8 weeks / 6 months). Empty state when no history. Files: `ExerciseModels.swift`, `WorkoutView.swift`. ### Note Stats are computed from the app's own per-day logs. Health (steps/kcal/resting HR + workout dates) is already read on the Today dashboard and merged into the streak; a Health overlay on this screen could be added later. --- ## KC-15 — Bars countdown widget + unified slate/orange widget theme **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Widgets ### Description Add a bar-meter event-countdown widget (per reference screenshot) and re-theme every widget to one look: dark slate background + brand orange, matching the app. ### Fix - `WidgetTheme` (slate background `RGB(0.224,0.255,0.31)`, brand orange accent). - `BarGrid` vertical-bar progress view; new `EventBarsView` + `EventBarsWidget` ("Event Countdown (Bars)", medium) reusing the existing event config/provider. - `EventEntry.fineTimeLeft` (days → "X hr, Y min left") for the bars label. - Re-themed all home-screen widgets to slate bg + orange: Day Progress, Tasks Done Today, Event Countdown (dots), My Tasks. Lock-screen accessory widgets stay on `.thinMaterial` (system-tinted on the Lock Screen). Files: `WidgetViews.swift`, `EventCountdownWidget.swift`, `MyTasksWidget.swift`, `KisaniCalWidgets.swift`. --- ## KC-16 — Workout check-in, missed state, and rest-day compensation **Status:** In Progress (implemented & build-verified; notification actions need a real device) **Reported by:** User **Area:** Workout / Notifications ### Description Workout reminders should ask "Have you done this workout?" with Yes / No / Snooze. Saying No marks that day missed and offers to compensate on a rest day. Long-press on workout/exercise cards gives quick Done / Not Done / Compensate actions. Each scheduled day keeps its own completion state. ### Fix - **VM:** `missedDates` + `compensations` (persisted + iCloud), `workoutStatus(on:)` (pending/done/missed), `markWorkoutDone/markWorkoutMissed(on:)` (per-date only), `upcomingRestDays`, `scheduleCompensation`, `promptCompensation`, `checkPendingMissed`. `program(for:)` honors compensations; a compensation day is not a rest day. - **Notifications:** WORKOUT_CONFIRM category now has "Yes, I completed it ✓", "No, I didn't" (→ marks missed + asks compensation on next open), and "Remind me in 1 hour" (snooze re-delivers). Daily reminder + check-in both use it. Compensation days get their own one-off reminder ("Missed workout recovery"). - **UI:** `CompensationSheet` (rest-day picker, "No, keep as missed"); long-press menus on the calendar/Today workout row (Done / Not Done / Compensate) and on exercise cards (Mark as Done / Not Done); workout row subtitle shows Pending / Done / Missed and a "Compensation ·" prefix (missed shows accent color). - Daily per-date rules unchanged (KC-1/KC-10): completing Tuesday never completes Saturday. Files: `ExerciseModels.swift`, `NotificationManager.swift`, `ContentView.swift`, `WorkoutView.swift`, `CalendarView.swift`, `TodayView.swift`. ### How to test (device) 1. Enable workout reminder/check-in in Settings; when it fires, use the actions. 2. "No, I didn't" → open app → compensation sheet appears → pick a rest day → that day now shows the program labeled Compensation (and gets a reminder). 3. Long-press the workout card in Today/Calendar → Done / Not Done / Compensate. 4. Long-press an exercise card in Workout → Mark as Done / Not Done. --- ## KC-17 — Remove Event Countdown (dots) + Tasks Done Today widgets **Status:** Resolved (build-verified) **Reported by:** User **Area:** Widgets ### Description The dot-grid "Event Countdown" and "Tasks Done Today" widgets were judged not up to standard and removed entirely from the gallery. ### Removal - `EventCountdownWidget` struct (kind `KisaniEventCountdown`) + `EventCountdownView`. - `TasksCompleteWidget` (kind `KisaniTasksComplete`) + `TasksCompleteView` + its only consumer `todayTaskCompletion()` in WidgetData.swift. - Both deregistered from `KisaniWidgetBundle`. - Kept (shared by the surviving bars widget): `EventConfigIntent`, `EventEntity`, `EventQuery`, `EventProvider`, `EventEntry`, `CycleCountdownUnitIntent`, `BarGrid`; `DotGrid` kept (used by Day Progress). - Untouched: Event Countdown (Bars), My Tasks, Day Progress, Lock Screen rect + circular, Task Live Activity. Files modified: `KisaniCalWidgets.swift`, `EventCountdownWidget.swift`, `WidgetViews.swift`, `WidgetData.swift`. No files deleted (shared code remains in EventCountdownWidget.swift). No app-side settings referenced these widgets. --- ## KC-18 — Bars widget: progress bar never fills; date pickers felt broken **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Widgets ### Symptoms The Event Countdown (Bars) widget showed all bars dim regardless of time left, and the Event date / Counting from rows in the config appeared not to work. ### Root causes 1. Auto (upcoming-queue) mode anchored progress to `Date()` on **every** refresh, so elapsed time was always ~0 → empty bar forever. 2. Custom mode defaulted both `Event date` and `Counting from` to the same instant (`Date.now` at config time) → zero-length span → progress 0. The raw second-precision defaults also made the picker rows look broken. 3. The timeline only refreshed at midnight, so even a correct bar wouldn't move during an "X hr left" countdown. ### Fix - Persistent per-event anchor (`kisani.widget.anchor.` in the App Group): progress fills from when an event was first tracked and resets naturally when the tracked event changes. An explicit "Counting from" before the event wins. - `targetDate`/`startDate` are now optional — rows read "Choose" until set, and a missing custom date falls back to a 2-day stub window. - Timeline pre-renders an entry every 30 minutes (12 h horizon) so the bar and time-left label keep moving. Files: `KisaniCalWidgets/EventCountdownWidget.swift`. Note: existing widget instances re-read their config; dates must be re-picked once (params changed to optional). --- ## KC-19 — Widgets: exact logo palette everywhere **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Widgets ### Description All widgets should match the brand orange theme of the logo exactly. ### Fix - `WidgetTheme` now uses the precise brand palette: accent **RGB(232,98,42)** (app `AppColors.accent`) on the logo's dark **RGB(26,29,34)** background — previously a near-miss orange on a slate background. - Every themed widget (Event Countdown Bars, My Tasks, Day Progress) inherits it. - Task Live Activity: hardcoded accent → `WidgetTheme.accent`; background tint → logo dark. - Lock-screen ring tints → brand accent (renders in color on StandBy/home). Files: `WidgetViews.swift`, `TaskLiveActivity.swift`. --- ## KC-20 — "Track Countdown": zero-config bars widget driven from the app **Status:** In Progress (implemented & build-verified, pending device build) **Reported by:** User **Area:** Widgets / Tasks ### Description The bars widget should not need manual event entry: track any KisaniCal event from the app and the widget displays it automatically (title, date, time left, progress bars). ### Fix - **App:** new `CountdownTracker` + a "Track Countdown" / "Stop Tracking Countdown" item in the shared task long-press menu. Stores `kisani.widget.trackedEvent` (+ `trackedSince`) in the App Group and reloads widget timelines. - **Widget:** the tracked event bypasses the widget configuration entirely. - **Mode A** (one-off): progress = elapsed/(event − tracking date). - **Mode B** (recurring birthdays/anniversaries): progress across the occurrence span containing now (previous → next occurrence), derived from the task's recurrence rule (`recurrenceSpan`). - `WidgetTask` now decodes `isRecurring`/`recurrenceLabel` from the shared JSON. - No tracked event → existing configuration (upcoming queue / picked / custom) remains the fallback, exactly as before. - Bar rendering already maps progress → filled (accent) vs remaining (dimmed) bars; the screenshot showing static dim bars was the pre-KC-18 build. Files: `TaskContextMenu.swift`, `WidgetData.swift`, `EventCountdownWidget.swift`. ### How to test (device) 1. Long-press any task with a due date → "Track Countdown". 2. The bars widget switches to that event without touching its config. 3. A yearly birthday shows Mode B: bar spans last year's → this year's date. 4. Long-press the task again → "Stop Tracking Countdown" → widget falls back to its configured mode. --- ## KC-21 — Eisenhower Matrix as a TickTick-style view (Priority × deadline) **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Tasks / Matrix ### Problem The Matrix treated importance as the task's *stored quadrant* — you placed a task in a row and only a manual "Move" changed it. TickTick instead treats the Matrix as a **view**: it auto-places every task from its **Priority** and **due date**, while still allowing quick drag/menu moves. ### Fix (model — `TaskItem.swift`) - Replaced whole-quadrant override with `urgencyOverride: Bool?`. - `nil` = urgency is computed from due date. - `true` = force urgent column. - `false` = force not-urgent column. - Importance is now `priority == .high` (top row), not the stored quadrant. Medium/Low/None stay in the bottom row. - Urgency is deadline-driven: - default urgent window = 3 days - birthday/domain/annual/renewal/subscription/exam/deadline = 14 days Auto-placement: Q1 = High+urgent, Q2 = High+later, Q3 = not-High+urgent, Q4 = the rest. - `moveToQuadrant` updates real attributes: - vertical movement sets Priority (top → High; moving down demotes High → Medium) - horizontal movement sets `urgencyOverride` only if the target column disagrees with date-derived urgency. - Editing due date (`setDate`) or saving the editor (`updateTask`) clears `urgencyOverride` so the task re-places live. - Category changes, priority changes, and date changes refresh the legacy `quadrant` color/widget fallback to match computed placement. - `addTask` applies KisaniCal defaults when no priority is picked: birthday / domain / annual (+ title keywords exam, subscription, renew, expir) → High; workout / gym / exercise → Medium. ### Fix (create UI — `TodayView.swift`) - `AddTaskSheet` no longer has a manual **quadrant** selector. It now exposes a **Priority** menu (flag icon). User picks Priority + Date only; placement is computed. Adding from a Matrix quadrant seeds High for the top row. - Fixed no-date creation: a task without an explicit parsed/picked date stays "No Date" instead of silently becoming today/urgent. ### Fix (Matrix UI — `MatrixView.swift`, `TaskContextMenu.swift`) - Completed tasks are hidden from the 2×2 Matrix grid by default, but remain in the per-quadrant detail Completed section. - Rows with an urgency override show a small **Manual** badge. - Context menu includes **Reset Matrix Urgency** when an urgency override exists. - Move menu shows all four quadrants, with a checkmark on the current quadrant. ### How to test 1. New task, no priority, far date → lands in Q4; raise to High → jumps to Q2. 2. New task with no date → stays in right column until given a due date/reminder. 3. Drag Q2 → Q1 when due date is far out → Manual badge appears. 4. Reset Matrix Urgency → task returns to its date-derived column. 5. Drag Q1 → Q4 → High demotes to Medium and urgency is overridden if needed. 6. Open the task, change its due date → urgency override clears, re-places by rule. 7. Add a "Mum's birthday" with no priority → auto-High and uses 14-day urgency window. --- ## KC-22 — Quick postpone / snooze from task menus and lock screen **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Tasks / Calendar / Matrix / Notifications ### Problem Long-pressing a task/event in Today, Matrix, or Calendar required going into the date editor or using only coarse "+1 day" actions. Lock-screen task reminders also only supported "Mark Complete". The user wanted standard task-manager snooze choices directly from the pressed item and notification actions. ### Fix (app task menus) - Added a shared **Postpone** submenu to `TaskMenuItems`: - Snooze 15 Minutes - Snooze 30 Minutes - Snooze 1 Hour - Snooze 2 Hours - Tomorrow - Wired the submenu through: - Today timeline + overdue/upcoming sections - Matrix grid + quadrant detail sections - Calendar day timeline - `TaskViewModel.postpone(_:minutes:)` applies the nudge: - existing reminder → reminder moves forward - due date with no reminder → due date/time moves forward and becomes timed - no date/reminder → creates a reminder ### Fix (lock screen notifications) - Task reminder notifications now include actions: - Snooze 15 min - Snooze 30 min - Snooze 1 hour - Snooze 2 hours - Notification response handler updates the stored task reminder and schedules a one-off snoozed notification for the selected delay. Files: `TaskContextMenu.swift`, `TaskItem.swift`, `TodayView.swift`, `MatrixView.swift`, `CalendarView.swift`, `NotificationManager.swift`. ### How to test 1. Long-press a task in Today → Postpone → 15 Minutes; reminder/due time moves. 2. Repeat from Matrix grid, Matrix detail, and Calendar day timeline. 3. Open Move from the same menu → only the other three quadrants are listed (see KC-25). 4. Trigger a task reminder notification on device → verify 15/30/60/120 minute lock-screen actions appear. 5. Tap a lock-screen snooze action → task reminder updates and notification re-delivers after the selected interval. --- ## KC-23 — "Pick Date" everywhere + sensible time-based reminders **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Tasks / Matrix / Calendar / Reminders ### Problem The Date submenu offered "Pick Date" only in the Today view — the Matrix grid cards and the Calendar reschedule/move menu were missing it. Separately, the reminder lead-time options were day/week based ("2 days early", "1 week early"), which is unrealistic for timed events; the user wanted the standard time-based set. ### Fix - "Pick Date" (and Edit) now appear in the Matrix grid cards and the Calendar day timeline menu — both open the shared `TaskEditSheet` via a new `editingTask` sheet (`QuadrantCard` gained an `onEdit`; Calendar passes `onEdit` into the task menu). - Reworked the reminder picker (`TaskDatePickerSheet`, shared by every entry point) from day-offsets to **minutes before the event time**: None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom wheel. Existing absolute `reminderDate`s migrate to the nearest minute offset on open. Files: `MatrixView.swift`, `CalendarView.swift`, `TodayView.swift`. ### How to test 1. Long-press a task in Matrix and in Calendar → Date → "Pick Date" is present. 2. Create/edit a task → reminder options read On time / 5 min / 30 min / 1 hour / 1 day / Custom, relative to the event time. 3. Custom → Minutes/Hours/Days toggle + count wheel commits the right offset. --- ## KC-24 — App rebrand to "Wenza" **Status:** In Progress (display name + copy done; App Store name set in ASC) **Reported by:** User **Area:** Branding ### Problem The app is being rebranded from "Kisani Cal" to **Wenza**. ### Fix - `CFBundleDisplayName` → "Wenza" (app) and "Wenza Widgets" (widget) in `project.yml`; permission prompts and in-app copy (sidebar, Auth, Onboarding, Settings, tutorial, widget description) updated to Wenza. - **Deliberately unchanged** to preserve App Store identity and user data: bundle ID `com.kutesir.KisaniCal`, App Group `group.com.kutesir.KisaniCal`, `kisani.tasks.v2.*` storage keys, team ID, and internal Xcode target/project names. - App Store listing name set separately in App Store Connect (ships with the next version). Files: `project.yml`, `ContentView.swift`, `TutorialManager.swift`, `AuthView.swift`, `OnboardingView.swift`, `SettingsView.swift`, `KisaniCalWidgets.swift`. ### How to test 1. Build & run → home-screen label and all in-app titles read "Wenza". 2. Confirm tasks/widget data survive (App Group + keys unchanged). --- ## KC-25 — Matrix "Move" lists only the other quadrants **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Tasks / Matrix ### Problem The Move submenu listed all four quadrants with a checkmark on the current one, so the quadrant a task already sat in appeared as a no-op option. ### Fix - `TaskMenuItems` Move submenu now excludes the task's current quadrant and lists only the other three, using the clear matrix labels (e.g. a task in Urgent & Important shows Not Urgent & Important, Urgent & Unimportant, Not Urgent & Unimportant). Shared across Today, Matrix, and Calendar. Files: `TaskContextMenu.swift`. ### How to test 1. Long-press a task in any quadrant → Move shows only the three other quadrants. 2. Pick one → task moves (priority/urgency updated per the TickTick model). --- ## KC-26 — Bars widget: auto-select advances to the next event **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Widgets ### Problem With "Upcoming queue" on + "Nearest to finish", the Event Countdown (Bars) widget stayed stuck on an event after it expired (e.g. a passed deadline) instead of rolling to the next-nearest. The selection was frozen: the timeline computed the chosen event once and reused it for all 24 pre-rendered entries, only refreshing every 12 hours. ### Fix - Each timeline entry now re-resolves the nearest-to-finish event *as of that entry's own time* (`upcomingEvents(asOf:)` filters `dueDate > entryDate`), so the moment the current event expires the next entry rolls to the next one. - The timeline refreshes the instant the current selection expires (`currentSelectionExpiry`), not 12 hours later, giving the next event a fresh full pre-render window. - Completing/closing a task already reloads widget timelines, and the recompute drops it and picks the next. ### Follow-up — tracked events now advance too The first pass left the **tracked** ("Track Countdown") path pinned: a completed or expired tracked event stayed on screen. Fixed with `trackedTask(asOf:)`, which returns the tracked event only while it's live (not complete, and recurring or still future); once done/past (non-recurring) the widget falls through to the next-nearest. Recurring tracked events still roll forward, and the refresh boundary now includes the tracked event's expiry. Files: `EventCountdownWidget.swift`. ### How to test 1. Queue on + Nearest to finish; let the nearest event's due time pass → widget rolls to the next-nearest without manual action. 2. Complete the nearest event → widget advances to the next. 3. Track an event, then complete it → widget advances to the next-nearest. --- ## KC-27 — Today ⋯ menu: Background, Group & Sort, Select **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Tasks / Today ### Problem Three items in the Today ⋯ menu were empty placeholders: Background, Group & Sort, and Select. ### Fix - **Background:** sheet with 5 themes (Default/Warm/Cool/Mono/Paper) applied as a subtle gradient to the Today background; persisted via `@AppStorage`. - **Group & Sort:** group by Date (default)/Priority/Category/Quadrant and sort by Smart/Due/Priority/Title. Non-date groupings render as titled sections reusing `UpcomingSection`. - **Select:** multi-select sheet with Select All / Deselect All and a bulk action bar (Complete / Priority / Move / Delete). - New file `TodayMenuFeatures.swift`; added `TaskViewModel.allActiveRows` so grouping/selection use the same single-occurrence task set (incl. undated). Files: `TodayMenuFeatures.swift`, `TodayView.swift`, `TaskItem.swift`. ### How to test 1. ⋯ → Background → pick a theme; Today background updates and persists. 2. ⋯ → Group & Sort → Priority/Category/Quadrant regroups the list. 3. ⋯ → Select → Select All → Complete marks all done. --- ## KC-28 — Clearer Snooze labels on notifications and task menus **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Notifications / Tasks ### Problem The lock-screen reminder actions and the in-app Postpone submenu showed bare durations ("15 min", "30 min", …) that didn't say what they do. iOS notification buttons are a flat list and can't nest, so a single expanding "Snooze" isn't possible — the labels must carry the meaning. ### Fix - Lock-screen actions: "Snooze 15 min / 30 min / 1 hour / 2 hours" (+ Mark Complete). - In-app Postpone submenu: "Snooze 15 Minutes / 30 Minutes / 1 Hour / 2 Hours". Files: `NotificationManager.swift`, `TaskContextMenu.swift`. --- ## KC-29 — Lock-screen Progress Dots widget legibility **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Widgets ### Problem The Progress Dots widget in the lock-screen accessoryRectangular family packed 100 tiny dots into a small pill over a custom dark background, which the lock screen's vibrant rendering washed out — it looked faint / not loaded. ### Fix - accessoryRectangular: 28 larger dots (vs 100), `.primary` color for the vibrant tint, and `AccessoryWidgetBackground()` for the standard frosted pill. Home Screen small/medium/large keep the 100-dot orange-on-dark grid. Files: `WidgetViews.swift`. --- ## KC-30 — Period milestone notifications (day / week / month / year) **Status:** In Progress (implemented & build-verified, pending device QA) **Reported by:** User **Area:** Notifications ### Problem The user wanted recurring "one more period passed" nudges (Pretty Progress style): day, week, month, and year, all at midnight. ### Fix - `schedulePeriodMilestones()` schedules recurring `UNCalendarNotificationTrigger`s, all firing at **midnight (00:00)**: - Today — "One more day passed." — every day - This Week — "One more week passed." — Monday 00:00 (weekly) - This Month — "One more month passed." — 1st at 00:00 (monthly) - This Year — "One more year passed." — Jan 1 at 00:00 (yearly) - Fixed identifiers + remove-then-add keep them de-duplicated; runs via the existing `reschedule(...)` path when authorized; gated by the `kisani.periodMilestones` flag (default on). - **Pending:** no in-app Settings toggle yet (see KC-31 #2). Files: `NotificationManager.swift`. ### How to test 1. Authorize notifications; at midnight a "One more day passed." notification fires (and the week/month/year equivalents on their boundaries). --- ## KC-31 — Audit fixes: lock-screen sync, live urgency, batch postpone **Status:** Fixed items build-verified; pending items remain open (see below) **Reported by:** User (code review) **Area:** Notifications / Tasks / Matrix ### Reviewed items & outcomes **Fixed** - **Lock-screen Mark Complete / Snooze now sync + reload.** `markTaskComplete` and `snoozeTask` previously wrote only to `UserDefaults.kisani`. They now also `CloudSyncManager.shared.push(...)` (→ iCloud / Apple Watch) and `WidgetCenter.shared.reloadAllTimelines()`. - **Notification copy uses live Matrix urgency, not stored quadrant.** `reschedule(...)` snapshots urgent task IDs from the computed `displayQuadrant` and threads them into `scheduleTasks` / `notifBody`, so "Urgent — tap to mark complete" and the evening check-in reflect current urgency rather than a stale stored fallback. - **Batch `postponeAllOverdue` matches single postpone.** It now clears `urgencyOverride` and re-syncs the stored quadrant per task. Files: `NotificationManager.swift`, `TaskItem.swift`. ### Pending (not closed) - **#2 — Period-milestone Settings toggle.** Currently only the `kisani.periodMilestones` UserDefaults flag (default on); no in-app UI to turn the day/week/month/year notifications on/off. **Pending.** - **#6 — Configurable Matrix thresholds & snooze durations.** `urgentWindowDays` (3), `categoryUrgentWindowDays` (14), and the 15/30/60/120-min snooze actions are hardcoded constants — fine for now, but no settings for TickTick-style customization. **Pending.** - **Note (#1) — Matrix "Move" lists only the other 3 quadrants.** This is the intended KC-25 behavior, not a defect; left as-is unless reversed. --- ## KC-32 — Calendar Day / 3-Day / Week / List views not showing tasks & events **Status:** Fixed (build + unit-test verified) **Reported by:** User **Area:** Calendar ### Description Tasks/events only appeared in the Month view. Day and 3-Day showed an empty timeline; Week showed abstract colored bars with no titles; the List/task-list views missed recurring tasks. ### Root cause - `cvTimeItems(date)` iterated raw `taskVM.tasks` (no recurrence) and dropped any untimed task via `guard h != 0 || m != 0` (midnight = "no time" → discarded). - Week rendered `eventDots` (colors only); List and the task-list toggle used raw `tasks` instead of `occurrences(on:)`. ### Fix - `cvTimeItems` now uses `occurrences(on:)` + the real `hasTime` flag; added an all-day band (`cvAllDayBand` / `cvAllDayItems`) for untimed tasks, all-day events, and the day's workout. - Week lists real titles (up to 4/day + "N more") via shared `cvAgendaItems`. - List view and `calDayTaskListView` switched to `occurrences(on:)`. Files: `CalendarView.swift`. --- ## KC-33 — Calendar month grid: weekday header & week numbers misaligned to firstWeekday **Status:** Fixed (17 unit tests, UI-verified) **Reported by:** User (release-blocking, data integrity) **Area:** Calendar ### Description Dates appeared under the wrong weekday labels (e.g. June 1 2026 — a Monday — shown under "S"). A data-integrity bug, not visual. ### Root cause The grid is built from `.weekOfMonth` (respects `Calendar.current.firstWeekday`), but the weekday header was hardcoded Sunday-first (`["S","M","T","W","T","F","S"]`) and the week-number label was gated on `isSunday` (weekday == 1). On any non-Sunday-first locale / "Start Week On = Monday", every column was mislabeled by one and the W-number landed on the wrong column. ### Fix - Extracted pure, testable date math into `CalendarGrid` (`monthGrid` + `weekdaySymbols`); `gridDays()` and the year-view mini-month dedupe to it. - Header now derived from `firstWeekday`, so it always matches the grid. - Week-number label gated on `isWeekStart` (weekday == firstWeekday). Documented that week numbers are locale `weekOfYear` (consistent with layout), not ISO. Files: `CalendarGrid.swift` (new), `CalendarView.swift`, `ContentView.swift` (DEBUG-only `KISANI_INITIAL_TAB` launch hook, compiled out of Release). ### Verification New `KisaniCalTests` target — 17 tests passing on simulator: exact Sunday-first June 2026 grid, header↔grid alignment for firstWeekday 1–7, cell identity, month navigation (May/Jul/Feb 2026 + Feb 2028 leap + Dec 2026→Jan 2027), event-day bucketing (all-day / late-night / midnight-crossing) across 4 timezones, and recurring-task occurrence expansion. Verified in the running app. --- ## KC-34 — Confirm before completing a future-dated task **Status:** Fixed (build-verified) **Reported by:** User **Area:** Tasks ### Description Tapping the circle on a task in "Next 7 Days" / "Later" silently completed it — easy to do by accident. ### Fix `TaskRowView` now shows a confirmation dialog before completing a task whose due date is after today (`isFutureTask`). Today/overdue tasks still toggle instantly, and un-completing never prompts. Applies everywhere `TaskRowView` is used. Files: `TodayView.swift`. --- ## KC-35 — Visual cleanup: slimmer checkbox + remove decorative emoji **Status:** Fixed (build-verified) **Area:** Today / Widget / Settings / Notifications ### Fix - Task checkbox: 22pt filled donut → 18pt open ring (1.5pt), no fill — quieter, matches the restrained brand. Tap target kept at 36×44. - Removed decorative emoji: 🎉 (My Tasks widget empty state), `𝕏 👾 📸` (Settings → Follow Us), `✓` (notification action title + "Done" body). Files: `TodayView.swift`, `MyTasksWidget.swift`, `SettingsView.swift`, `NotificationManager.swift`. --- ## KC-36 — Version display reads from the bundle (no hardcoded "v1.0.0") **Status:** Fixed (build-verified) **Area:** Settings / Build ### Description About row and About sheet hardcoded "v1.0.0" while the app was 2.0 — drift. ### Fix Added a `Bundle` extension (`marketingVersion` / `buildNumber` / `versionDisplay`); both spots now read from the bundle (driven by `project.yml`). Build set to **2.0 (9)**. Files: `SettingsView.swift`, `project.yml`. --- ## KC-37 — Quick-add recurrence: detect "everyday" and "every weekday" **Status:** Fixed (6 parser tests) **Reported by:** User **Area:** Tasks / Natural-language parsing ### Description Typing "remind me to pray everyday" didn't set a Daily recurrence. ### Root cause `parseRecurrence` required a space (`\bevery\s+day\b`), so "everyday" (one word) was missed; "every weekday" wasn't handled. ### Fix `\bevery\s*day\b` now matches both "every day" and "everyday"; added "each day", and "Every Weekday" detection ("every weekday" / "weekdays") ahead of the broader weekly/daily patterns. Sub-day cadence ("every hour" / "hourly") is intentionally **not** matched — the occurrence engine is day-granular, so it stays a one-off rather than being mislabeled. Files: `TodayView.swift` (`NLTaskParser`). 6 parser tests added. ### Pending (follow-up) - **Hourly / sub-day recurrence** is unsupported by the day-granular occurrence engine. Would need an engine + repeating-notification extension. **Pending.** --- ## KC-38 — Voice input: add tasks by speaking **Status:** Implemented (committed `eb7d563`) **Area:** Tasks / Quick-add ### Description The quick-add mic button is now wired to live, on-device speech recognition so a task can be dictated instead of typed. ### Implementation - New `SpeechRecognizer` (`Speech` + `AVFoundation`): `SFSpeechRecognizer` driven by `AVAudioEngine`, publishing `transcript` / `isRecording` / `error`. Requests speech + microphone authorization, then streams partial results. - `TodayView` quick-add: mic button calls `speech.toggle()` (mic → mic.fill, accent + pulse while recording); `onChange(of: speech.transcript)` binds the text into the task field, so dictation flows through the **same `NLTaskParser`** — spoken dates/recurrence (e.g. "remind me to pray everyday") parse exactly like typed input (see KC-37). Permission errors surface inline. - Usage strings added: `NSMicrophoneUsageDescription`, `NSSpeechRecognitionUsageDescription`. Files: `SpeechRecognizer.swift` (new), `TodayView.swift`, `project.yml`. ### Notes / follow-up - Recognition locale is `.current`; no manual language picker. - Contains `print("[VOICE] …")` debug logging — harmless, could be removed before release. **Minor.** ### Follow-up fix — transcript never appeared in the field **Status:** Fixed (simulator-verified) The mic captured speech and the transcript reached `rawText`, but the text never showed in the Quick Add field. Root cause: `NLTaskField.updateUIView` (`UIViewRepresentable` over `UITextView`) had `guard coordinator.isEditing else { …return }` — so when the field wasn't focused (the normal case when tapping the mic), externally-set text (voice) was dropped and the placeholder stayed. Fixed to render non-empty `text` even when not editing (caret moved to end). Verified on simulator by pushing text through the real `speech.transcript` path: "Buy milk tomorrow" → field shows it, date chip = Tomorrow; "Call Romeo at 11 AM" → field shows it, chip = Today 11:00. Not exercised on the sim (no microphone / no UI-tap automation): live mic capture and the literal Save tap. The save path is the same `submit()`/`addTask` used by all typed tasks, so it's covered by normal task creation. Added DEBUG-only test hooks (`KISANI_AUTOADD`, `KISANI_VOICE_SIM`) to inject a transcript on the simulator; `#if DEBUG`, compiled out of Release. Files: `TodayView.swift` (`NLTaskField.updateUIView`), `ContentView.swift`. --- ## KC-39 — Workout state lost on reinstall/update (incomplete iCloud restore) **Status:** Fixed (build-verified) **Reported by:** User **Area:** Workout / iCloud sync ### Description Workout state was lost after app updates / fresh installs. ### Root cause `save()` pushes 7 workout keys to iCloud KVS (programs, schedule, activePid, completedDates, history, missedDates, compensations), but `WorkoutViewModel.init` only `restoreIfMissing(...)` for the first four. So **history, missed dates, and rest-day compensations** were uploaded to iCloud but never restored on a fresh launch — silently lost on reinstall/update. ### Fix Added the three missing `restoreIfMissing` calls (`history`, `missedDates`, `compensations`) so restore is symmetric with save. (Per-day `lastActiveDay` is transient set-reset state and intentionally not synced.) Files: `ExerciseModels.swift`. ### Note Cross-device *live* refresh while the app is open isn't wired (`WorkoutViewModel` doesn't observe `kisaniCloudDataRefreshed`) — out of scope; the restore-on-launch path is what preserves state across updates. --- ## KC-40 — Workout streak resets on a missed day (should be a lifetime tally) **Status:** Implemented (not build-verified — sim runtime unavailable) **Reported by:** User **Area:** Workout ### Description The profile showed a streak of `1` even after 5 workouts that week — a single skipped/rest day reset it. User wants the count to only ever grow: every workout achieved counts, a 4/5 week and a 5/5 week both count fully, misses never subtract. ### Change `streakDays` is now a lifetime tally of distinct workout days (`Set(workoutDates).count`, `totalAchievedWorkoutDays`). Removed the consecutive/weekly-goal chain. Settings copy updated (goal no longer "maintains" a breakable streak). Unit tests in `StreakLogicTests` (4/5+5/5=9, blank-week, duplicates, skipped-day, empty) — algorithm also verified via standalone harness. Files: `ExerciseModels.swift`, `SettingsView.swift`, `KisaniCalTests/StreakLogicTests.swift`. --- ## KC-41 — 7-day bars empty & "this week" = 0 despite a nonzero done rate **Status:** Implemented (not build-verified) **Reported by:** User **Area:** Tasks / Profile stats ### Description The profile's "COMPLETED LAST 7 DAYS" bars stayed flat and "this week" showed 0 even with completed tasks, because only one-off `completedAt` tasks were counted — recurring occurrences were ignored. ### Change Added `TaskViewModel.completionCount(on:)` / `completions(on:)` that count recurring occurrences (`completedOccurrences`) plus one-offs. Profile bars, "this week", and a new "day streak" cell now use them. Files: `TaskItem.swift`, `SettingsView.swift`. --- ## KC-42 — No way to browse historical task/workout activity **Status:** Implemented (not build-verified) **Reported by:** User ("we need to be able to dig through the data") **Area:** Tasks / Workout / History ### Change New `ActivityHistoryView`: a 90-day, day-by-day archive of completed tasks (with times, recurring markers), workout logs (sets + volume), and missed / made-up days. Opened via a "History" button on the profile Tasks card. Files: `ActivityHistoryView.swift` (new), `SettingsView.swift`. --- ## KC-43 — Today-list checkbox color duplicates the row accent **Status:** Implemented (not build-verified) **Reported by:** User **Area:** Tasks / Today ### Change `TaskRowView` checkbox stroke changed from `task.quadrant.color` to neutral `text3` — the row already carries its color on the left accent bar and the category chip. Calendar day-list checkbox left colored (no accent bar there). Files: `TodayView.swift`. --- ## KC-44 — Workout streak banner should show week, then year, then comparison **Status:** Implemented (not build-verified) **Reported by:** User **Area:** Workout ### Change `StreakBannerView` is now a swipeable 3-page TabView: (1) this week + 7 day ticks, (2) this year + 12 month ticks + all-time total, (3) this-week-vs-last-week delta. Added VM breakdowns: `workoutsThisWeek/LastWeek/ThisYear`, `currentWeekDays`, `currentYearMonths`. Files: `WorkoutView.swift`, `ExerciseModels.swift`. --- ## KC-45 — Calendar year view should scroll continuously (TickTick-style) **Status:** Implemented (not build-verified) **Reported by:** User **Area:** Calendar ### Change Replaced the single paged year (`◀ 2026 ▶`, dead space below December) with an infinite `LazyVStack` of years, anchored on the current year (accent-colored). The top header now follows the scrolled-to year via a `YearTopKey` preference (iOS 16-safe). Removed `navigateYear` + the year nav header. Files: `CalendarView.swift`. --- ## KC-46 — Calendar month view: TickTick bars + collapse (REVERTED) **Status:** Reverted (user preferred the original) **Reported by:** User **Area:** Calendar ### Notes Implemented TickTick-style day bars, collapse-to-week on selection (tap/drag), and a full-column selection highlight (commits `2ec3278`, `bd43eec`). User preferred the original month view, so both were reverted (commits `0a87769`, `e8d74d4`). Month view is back to dots + full grid + agenda below. Files: `CalendarView.swift`. --- ## KC-47 — Calendar week view should be a TickTick-style timeline **Status:** Implemented (not build-verified) **Reported by:** User (screen recording) **Area:** Calendar ### Change Replaced the mini-month + agenda-list split with a 7-day timeline (hours down the left, one column per weekday, events as positioned blocks) reusing `cvTimeGrid`/`cvDayColumnHeader`. Added: - **Drag-to-reschedule**: task blocks drag vertically to change time, snapped to 15 min (`CVTimeBlock`, `rescheduleTask`; events + recurring tasks stay fixed). - **Layout toggle**: timeline ⇄ grid of day-cards (`weekGrid`, `weekGridLayout`). - Week-swipe navigation (`navigateWeek`). Known gaps: drag moves time only (not across days); calendar events not draggable. Files: `CalendarView.swift`. --- ## KC-48 — Week / 3-day views float to the bottom (dead space on top) **Status:** Fixed (not build-verified) **Reported by:** User **Area:** Calendar / Layout ### Root cause The timeline views are a thin day-header + a greedy `ScrollView` (≈0 ideal height), so the top-level `VStack` shrank and the `ZStack(alignment: .bottomTrailing)` floated it to the bottom, leaving a large empty gap above the header. Month view has a tall fixed grid, so it never surfaced. ### Fix Pinned the top-level content `VStack` with `.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)`. Files: `CalendarView.swift`. --- ## KC-49 — Week/3-day dead space persisted after KC-48 **Status:** Fixed (not build-verified) — supersedes KC-48 **Reported by:** User ("DEAD SPACE STILL EXISTS") **Area:** Calendar / Layout ### Root cause (refined) Pinning only the outer container (`.frame(maxHeight: .infinity)`, KC-48) merely *allows* fill — it doesn't *force* it. The timeline `ScrollView` returned its content size instead of expanding, so the week/3-day root stayed short and the `.bottomTrailing` ZStack floated it down, leaving the day header stranded mid- screen. Day view happened to fill, so it never showed. ### Fix Force the timeline `ScrollView` to be greedy with `.frame(maxWidth: .infinity, maxHeight: .infinity)`, and pin each timeline view's root `VStack` to fill + top-align. Applied to `weekView` and `threeDayView`. Files: `CalendarView.swift`. --- ## KC-50 — Long-press on a timeline event has no actions **Status:** Implemented (not build-verified) **Reported by:** User **Area:** Calendar / Tasks ### Description Long-pressing a task block on the day/week/3-day timeline did nothing — it should offer the same actions as elsewhere (Complete, Snooze, reschedule, etc.). ### Change `CVTimeBlock` now takes `@ViewBuilder menu` content and shows it via `.contextMenu`. `cvBlockMenu(for:)` builds it for task-backed items: a Complete toggle, a Snooze submenu (15 min / 1 hr / tomorrow), then the shared `TaskMenuItems` (pin, reschedule, move, priority, category, edit, delete). Events (no `taskID`) get no menu. Files: `CalendarView.swift`.