2026-06-06 19:29:25 +03:00
|
|
|
|
# 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
|
2026-06-06 19:29:25 +03:00
|
|
|
|
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`.
|
2026-06-06 19:29:25 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-06-06 19:29:25 +03:00
|
|
|
|
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
|
2026-06-06 19:46:39 +03:00
|
|
|
|
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).
|
2026-06-07 04:08:23 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|
2026-06-07 14:11:26 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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>.app/Info.plist`
|
|
|
|
|
|
and `ls <App>.app | grep LaunchScreen` (expect `LaunchScreen.storyboardc`).
|
|
|
|
|
|
|
|
|
|
|
|
Committed: fix `72f4566`, build-number bump `345415e`.
|
2026-06-07 23:12:46 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.<uid>` (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`.
|
2026-06-08 00:29:48 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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`.
|
2026-06-09 01:41:15 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
2026-06-09 10:47:13 +03:00
|
|
|
|
**Status:** In Progress (implemented & build-verified, pending device build)
|
2026-06-09 01:41:15 +03:00
|
|
|
|
**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`.
|
|
|
|
|
|
|
2026-06-09 10:47:13 +03:00
|
|
|
|
### "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`.
|
2026-06-10 10:09:55 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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).
|
2026-06-10 10:21:12 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|
2026-06-10 17:31:55 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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`.
|
Workout check-in: missed state + rest-day compensation (KC-16)
- Notification actions on workout reminders/check-ins: "Yes, I completed
it", "No, I didn't" (marks the day missed, then asks to compensate on a
rest day), "Remind me in 1 hour" (snooze).
- VM: missedDates + compensations (persisted/synced), per-date
markWorkoutDone/Missed, workoutStatus, upcomingRestDays,
scheduleCompensation, promptCompensation, checkPendingMissed.
program(for:) returns a compensation program on its rest day.
- CompensationSheet: pick a rest day or keep as missed; compensation days
get a one-off "Missed workout recovery" reminder.
- Long-press menus: workout row (Done / Not Done / Compensate) in Today +
Calendar; exercise cards (Mark as Done / Not Done). All actions are
per-date — one occurrence never affects another.
- Workout row shows Pending / Done / Missed + "Compensation" label.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:06:58 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|
2026-06-11 11:49:35 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|
2026-06-11 18:16:30 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.<id>` 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).
|
2026-06-11 19:33:01 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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`.
|
2026-06-11 23:05:33 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|
2026-06-15 12:40:39 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## KC-21 — Eisenhower Matrix as a TickTick-style view (Priority × deadline)
|
|
|
|
|
|
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
**Status:** In Progress (implemented & build-verified, pending device QA)
|
|
|
|
|
|
**Reported by:** User
|
|
|
|
|
|
**Area:** Tasks / Matrix
|
|
|
|
|
|
|
2026-06-15 12:40:39 +03:00
|
|
|
|
### 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
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
date**, while still allowing quick drag/menu moves.
|
2026-06-15 12:40:39 +03:00
|
|
|
|
|
|
|
|
|
|
### Fix (model — `TaskItem.swift`)
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
- Replaced whole-quadrant override with `urgencyOverride: Bool?`.
|
|
|
|
|
|
- `nil` = urgency is computed from due date.
|
|
|
|
|
|
- `true` = force urgent column.
|
|
|
|
|
|
- `false` = force not-urgent column.
|
2026-06-15 12:40:39 +03:00
|
|
|
|
- Importance is now `priority == .high` (top row), not the stored quadrant.
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
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
|
2026-06-15 12:40:39 +03:00
|
|
|
|
Auto-placement: Q1 = High+urgent, Q2 = High+later, Q3 = not-High+urgent,
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
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.
|
2026-06-15 12:40:39 +03:00
|
|
|
|
- `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.
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
- 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.
|
2026-06-15 12:40:39 +03:00
|
|
|
|
|
|
|
|
|
|
### How to test
|
|
|
|
|
|
1. New task, no priority, far date → lands in Q4; raise to High → jumps to Q2.
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
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`:
|
2026-06-16 02:12:52 +03:00
|
|
|
|
- Snooze 15 Minutes
|
|
|
|
|
|
- Snooze 30 Minutes
|
|
|
|
|
|
- Snooze 1 Hour
|
|
|
|
|
|
- Snooze 2 Hours
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
- 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:
|
2026-06-16 02:12:52 +03:00
|
|
|
|
- Snooze 15 min
|
|
|
|
|
|
- Snooze 30 min
|
|
|
|
|
|
- Snooze 1 hour
|
|
|
|
|
|
- Snooze 2 hours
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
- 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.
|
2026-06-15 19:53:14 +03:00
|
|
|
|
3. Open Move from the same menu → only the other three quadrants are listed (see KC-25).
|
Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the
Calendar reschedule/move menu, matching the tasks view — both open the shared
TaskEditSheet via a new editingTask sheet.
- Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points)
from day/week offsets to sensible time-based ones relative to the event time:
None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom
wheel. Existing absolute reminderDates migrate to the nearest minute offset.
- Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task
reminder; postpone now clears the Matrix urgency override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:14:10 +03:00
|
|
|
|
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.
|
2026-06-15 19:53:14 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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
|
2026-06-16 02:43:31 +03:00
|
|
|
|
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.
|
2026-06-15 19:53:14 +03:00
|
|
|
|
|
|
|
|
|
|
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.
|
2026-06-16 02:43:31 +03:00
|
|
|
|
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`.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-17 00:03:22 +03:00
|
|
|
|
## KC-30 — Period milestone notifications (day / week / month / year)
|
2026-06-16 02:43:31 +03:00
|
|
|
|
|
|
|
|
|
|
**Status:** In Progress (implemented & build-verified, pending device QA)
|
|
|
|
|
|
**Reported by:** User
|
|
|
|
|
|
**Area:** Notifications
|
|
|
|
|
|
|
|
|
|
|
|
### Problem
|
2026-06-17 00:03:22 +03:00
|
|
|
|
The user wanted recurring "one more period passed" nudges (Pretty Progress
|
|
|
|
|
|
style): day, week, month, and year, all at midnight.
|
2026-06-16 02:43:31 +03:00
|
|
|
|
|
|
|
|
|
|
### Fix
|
2026-06-17 00:03:22 +03:00
|
|
|
|
- `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)
|
2026-06-16 02:43:31 +03:00
|
|
|
|
- Fixed identifiers + remove-then-add keep them de-duplicated; runs via the
|
|
|
|
|
|
existing `reschedule(...)` path when authorized; gated by the
|
2026-06-17 00:03:22 +03:00
|
|
|
|
`kisani.periodMilestones` flag (default on).
|
|
|
|
|
|
- **Pending:** no in-app Settings toggle yet (see KC-31 #2).
|
2026-06-16 02:43:31 +03:00
|
|
|
|
|
|
|
|
|
|
Files: `NotificationManager.swift`.
|
|
|
|
|
|
|
|
|
|
|
|
### How to test
|
2026-06-17 00:03:22 +03:00
|
|
|
|
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.
|
2026-06-24 21:05:33 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.**
|
2026-06-24 21:13:26 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.**
|
2026-06-24 21:22:45 +03:00
|
|
|
|
|
|
|
|
|
|
### 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`.
|
2026-06-24 21:52:16 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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.
|
docs(issues): log KC-40–48 (streak tally, stats, history, banner, calendar rework)
Records this session's work in the issue tracker: workout streak → lifetime
tally (KC-40), recurring-aware stats (KC-41), Activity History (KC-42), neutral
Today checkbox (KC-43), swipeable streak banner (KC-44), continuous year view
(KC-45), month TickTick bars+collapse reverted (KC-46), week timeline + drag +
grid toggle (KC-47), week/3-day dead-space fix (KC-48).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:17:18 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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`.
|
2026-07-07 01:35:25 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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`.
|
docs(issues): KC-51 — analytics system plan, persistence eval, blockers
Records the /goal analytics-system implementation plan: reusable-functionality
survey, SwiftData/CoreData/current persistence evaluation, proposed architecture
(stores, immutable reports, pure analysis engine, reconciliation, notifications,
UI, tests), and the two blockers (build-verification unavailable; persistence +
min-iOS decision).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:42:45 +03:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## KC-51 — Health & Workout Analytics system (12-month, immutable reports) — PLAN
|
|
|
|
|
|
|
|
|
|
|
|
**Status:** Planning (blocked on 2 decisions + build-verification environment)
|
|
|
|
|
|
**Reported by:** User (/goal)
|
|
|
|
|
|
**Area:** Analytics / Persistence / HealthKit / Notifications
|
|
|
|
|
|
|
|
|
|
|
|
### Reusable existing functionality (survey)
|
|
|
|
|
|
- `WorkoutViewModel`: `history: [WorkoutDayLog]`, `workoutDates`, `schedule`,
|
|
|
|
|
|
`missedDates`, `compensations`; `StatPeriod`/`WorkoutStat`/`StatBucket`
|
|
|
|
|
|
aggregation already exists (`stat(period:offset:)`, `statBuckets`).
|
|
|
|
|
|
- `WorkoutDayLog` → `LoggedExercise` → `LoggedSet(weight, reps, done)` with
|
|
|
|
|
|
computed `volume` (Σ weight×reps for done sets), `totalSets`, `doneSets`.
|
|
|
|
|
|
- `HealthKitManager`: steps, active calories, resting HR, workoutsThisWeek,
|
|
|
|
|
|
`fetchWorkoutDates(from:to:)`, `saveWorkout`, `sumQuery`/`latestQuery`.
|
|
|
|
|
|
- `NotificationManager`: categories/actions, foreground `reschedule(...)`, and
|
|
|
|
|
|
**`schedulePeriodMilestones()`** (week/month/year-close nudges) — extend this.
|
|
|
|
|
|
- Lifecycle reconcile pattern already used: `checkPendingMissed` /
|
|
|
|
|
|
`checkPendingHealthConfirm` on `onAppear` + `scenePhase == .active`.
|
|
|
|
|
|
- Persistence: `UserDefaults` mirrored to iCloud KVS (`CloudSyncManager`).
|
|
|
|
|
|
|
|
|
|
|
|
### Persistence evaluation (SwiftData / Core Data / current)
|
|
|
|
|
|
- **Current (JSON blobs in UserDefaults + iCloud KVS):** iCloud KVS has a hard
|
|
|
|
|
|
**1 MB total** ceiling (see KC-39 context). 12 months of daily snapshots +
|
|
|
|
|
|
HealthKit reference cache **will exceed it** → silent sync failure. Not viable
|
|
|
|
|
|
as the analytics store.
|
|
|
|
|
|
- **SwiftData:** cleanest modelling/migration, but requires **iOS 17+**. App
|
|
|
|
|
|
deploymentTarget is **iOS 16.0** → SwiftData is UNAVAILABLE unless the min
|
|
|
|
|
|
target is raised (drops iOS 16 devices — a product call).
|
|
|
|
|
|
- **Core Data:** works at iOS 16, unbounded local storage, supports lightweight
|
|
|
|
|
|
migration; more boilerplate. Local-only (analytics need not sync via KVS;
|
|
|
|
|
|
reports are device-derived and reconstructable).
|
|
|
|
|
|
|
|
|
|
|
|
### Proposed architecture (pending the decision below)
|
|
|
|
|
|
- **Stores:** `WorkoutRecordStore` (app-generated, mirrors `WorkoutDayLog`) +
|
|
|
|
|
|
`HealthReferenceStore` (HealthKit cache; HealthKit = source of truth) +
|
|
|
|
|
|
`ReportStore` (immutable daily snapshots, weekly, monthly). Keep app records
|
|
|
|
|
|
and HealthKit reference separate; analysis reads across both.
|
|
|
|
|
|
- **Immutability:** reports store a frozen snapshot at generation; later edits to
|
|
|
|
|
|
source data never mutate a generated report. Retention: prune raw beyond 12
|
|
|
|
|
|
months; keep report summaries.
|
|
|
|
|
|
- **Analysis engine (`AnalyticsEngine`)**: pure, no SwiftUI/persistence deps,
|
|
|
|
|
|
deterministic. Formulas documented: volume = Σ(weight×reps) of completed sets;
|
|
|
|
|
|
%Δ = (cur−base)/|base| guarded for base≈0/new/rest; consistency = sessions ÷
|
|
|
|
|
|
scheduled over window; trend = classify(slope, threshold) → improve / decline /
|
|
|
|
|
|
plateau / insufficient-data. Handles missing/partial HK, deloads, outliers
|
|
|
|
|
|
(winsorize/IQR), unit compatibility, divide-by-zero. Non-medical language +
|
|
|
|
|
|
disclaimer; never diagnoses.
|
|
|
|
|
|
- **Comparisons:** daily vs previous comparable day + same weekday −1 week;
|
|
|
|
|
|
weekly vs previous completed week; monthly vs previous month.
|
|
|
|
|
|
- **Reconciliation:** idempotent generators keyed by period id; on launch/active,
|
|
|
|
|
|
backfill any missing completed week/month; never duplicate reports or
|
|
|
|
|
|
notifications. Do not depend on exact BG execution.
|
|
|
|
|
|
- **Notifications:** extend `schedulePeriodMilestones()`; one permission-aware,
|
|
|
|
|
|
user-scheduled local notification per weekly/monthly report; deep-link via
|
|
|
|
|
|
`userInfo` → route to the specific report (new deep-link cases).
|
|
|
|
|
|
- **UI (`Analytics` area):** Overview · Daily comparison · Weekly reports ·
|
|
|
|
|
|
Monthly reports · 12-month trends · Per-exercise history. Full loading / empty /
|
|
|
|
|
|
denied-permission / partial-data / error states; Dynamic Type, VoiceOver,
|
|
|
|
|
|
light/dark, compact/regular; Wenza design tokens; charts only where they aid
|
|
|
|
|
|
understanding; non-color status indicators.
|
|
|
|
|
|
- **Tests:** date boundaries (daily/weekly/monthly), calendar/locale/firstWeekday/
|
|
|
|
|
|
timezone/DST, volume/%/consistency/trend math, missing/partial HK, migration/
|
|
|
|
|
|
dedup/reconcile/retention, exercise identity + unit compat, report+notification
|
|
|
|
|
|
idempotency, deep-link routing.
|
|
|
|
|
|
|
|
|
|
|
|
### BLOCKERS (must resolve before build-verified completion)
|
|
|
|
|
|
1. **Build/test verification is impossible in this environment.** The iOS 26.5
|
|
|
|
|
|
simulator runtime was removed this session and CLI builds are broken, so
|
|
|
|
|
|
completion conditions #10 (tests pass) and #11 (Xcode build succeeds) CANNOT
|
|
|
|
|
|
be verified here. Per "never claim verification not performed", any code
|
|
|
|
|
|
produced now is inspection-only until a runtime is restored / built on device.
|
|
|
|
|
|
2. **Persistence + min-iOS decision (irreversible):** SwiftData needs iOS 17
|
|
|
|
|
|
(app is iOS 16). Migrating live workout history to a new store is a one-way
|
|
|
|
|
|
data migration. Needs the founder's call before implementation.
|
|
|
|
|
|
|
|
|
|
|
|
### Next action
|
|
|
|
|
|
Awaiting decision on persistence backend / deployment target, then implement the
|
|
|
|
|
|
pure `AnalyticsEngine` + models + tests first (most inspection-verifiable), UI
|
|
|
|
|
|
last. Device/build QA tracked here.
|
analytics(engine): pure deterministic AnalyticsEngine + models + tests (KC-51 ph1)
Phase 1 of the analytics system (/goal): the framework-independent analysis
engine. Value-type models (MetricDelta, TrendResult, DaySample, WeeklyReport,
MonthlyReport, ...) and AnalyticsEngine with documented formulas (volume,
%change, consistency, trend) and honest degradation (nil/zero/tiny baseline,
new exercise, rest day, outliers via winsorize, unit-dimension compatibility,
divide-by-zero). Calendar math takes an injected Calendar (locale/firstWeekday/
timezone/DST-safe).
AnalyticsEngineTests: 35 XCTest cases. Independently verified by compiling the
engine with swiftc and running a 42-assertion harness — all pass. (Xcode test
target not runnable in this env; assertions mirror the XCTest file.)
Registered the new files via xcodegen. Logs KC-51 phase-1 progress in ISSUES.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:06:59 +03:00
|
|
|
|
|
|
|
|
|
|
### KC-51 progress — Phase 1: analysis engine (DONE, engine-verified)
|
|
|
|
|
|
- Added pure, deterministic `AnalyticsEngine` + value-type models
|
|
|
|
|
|
(`AnalyticsModels.swift`) — no SwiftUI/persistence/HealthKit deps.
|
|
|
|
|
|
- Formulas documented in-source: volume, %change (guarded), consistency, trend.
|
|
|
|
|
|
- Honest degradation: nil/zero/tiny baseline → no %; new exercise / rest day →
|
|
|
|
|
|
not meaningful; multi-point trend vs least-squares slope; winsorized outliers;
|
|
|
|
|
|
unit-dimension compatibility; divide-by-zero guarded.
|
|
|
|
|
|
- Calendar helpers take an injected `Calendar` → locale/firstWeekday/TZ/DST-safe.
|
|
|
|
|
|
- `AnalyticsEngineTests.swift` (35 cases). **Verified independently**: compiled
|
|
|
|
|
|
with `swiftc` and ran a 42-assertion harness → all pass (Xcode test target not
|
|
|
|
|
|
runnable here — no sim runtime; same assertions live in the XCTest file).
|
|
|
|
|
|
- Next: Phase 2 file-based persistence (report/record stores), adapters from
|
|
|
|
|
|
`WorkoutDayLog` + HealthKit, reconciliation, retention, dedup — with tests.
|
2026-07-07 22:34:32 +03:00
|
|
|
|
|
|
|
|
|
|
### KC-51 progress — Phase 2: persistence + reconciliation (DONE, store-verified)
|
|
|
|
|
|
- `AnalyticsStore` (file-based JSON under Application Support): immutable frozen
|
|
|
|
|
|
daily snapshots + immutable weekly/monthly reports (writeIfAbsent →
|
|
|
|
|
|
idempotent, deduped by period key), retention prune (>12mo), no raw HealthKit
|
|
|
|
|
|
persisted (only derived reference values in DaySample).
|
|
|
|
|
|
- Engine reconciliation selectors: `completedWeekStartKeys`, `completedMonthKeys`,
|
|
|
|
|
|
`missingKeys` (idempotent — already-generated periods skipped).
|
|
|
|
|
|
- `AnalyticsStoreTests` (6 cases). **Verified independently**: compiled store +
|
|
|
|
|
|
engine with swiftc and ran a 23-assertion temp-dir harness → all pass.
|
|
|
|
|
|
- Next: Phase 3 app integration — adapter (`WorkoutDayLog` + HealthKit →
|
|
|
|
|
|
DaySample), reconcile coordinator on launch/active, notifications + deep links.
|
|
|
|
|
|
Phase 4: Analytics UI (6 areas + states + accessibility). Both require an Xcode
|
|
|
|
|
|
build / device to verify (no sim runtime here).
|