Build error (Xcode, Swift 6 strict concurrency): WorkoutSnapshot.init(vm:) read
history/workoutDates/schedule/missedDates from @MainActor WorkoutViewModel in a
nonisolated init. The init is only ever called from AnalyticsService.reconcile
(@MainActor) — mark it @MainActor to match its actual call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes 'available HealthKit trends' for weekly/monthly reports. Adds
HealthKitManager.dailyReference (3 parallel bucketed HKStatisticsCollectionQuery
for steps/active-calories/resting-HR over ~13 months), AnalyticsService.
refreshHealthReference to cache and merge it into DaySamples, and avgSteps/
avgActiveCalories/avgRestingHR on PeriodSummary (nil when absent — honest).
Surfaced in AnalyticsView's weekly/monthly cards.
Verified standalone (swiftc, 8/8): aggregation correct when present, nil when
absent, workout metrics unaffected, reports stay deterministic. Tests added to
AnalyticsEngineTests. This completes all analytics logic — remaining work is the
first Xcode build (device, user-run) to catch any SwiftUI compile issues.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves sample-building to nonisolated static funcs over a captured WorkoutSnapshot
so the coordinator's escaping closures don't touch @MainActor state — avoids a
main-actor isolation error. Matches the app's existing @MainActor + static shared
pattern (HealthKitManager).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes deep-link routing (goal #7): AnalyticsDeepLink is Identifiable; a
tapped weekly/monthly report notification is consumed in ContentView on
launch/active and presents AnalyticsView opened to that area. Core round-trip
re-verified standalone; SwiftUI wiring needs an Xcode build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-contained Analytics UI: Overview/Daily/Weekly/Monthly/12-Month/Exercises
with color+glyph status, empty/partial states, Swift Charts, VoiceOver labels,
Wenza tokens, and the non-medical disclaimer. Entry via an IButton in the
Workout header (sheet). Added Identifiable chart points.
Self-reviewed for compile issues (fixed tuple key-paths, a11y trait literal,
format string) but NOT build-verified — no iOS runtime; SwiftUI/app deps. Logged
in ISSUES.md as device-required.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the verified analytics core into the app: AnalyticsService feeds live
WorkoutViewModel data into the reconcile coordinator and exposes UI reads;
ContentView reconciles on launch + foreground; NotificationManager fires one
permission-aware, deduped weekly/monthly report notification carrying an
AnalyticsDeepLink, captured on tap for routing.
NOT build-verified (no iOS runtime; app-module/SwiftUI deps). Logged in
ISSUES.md; historical HealthKit reference + user-scheduled notification time are
follow-ups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3 core (verifiable): WorkoutAnalyticsAdapter (app data → DaySample),
AnalyticsCoordinator (idempotent backfill of missing weekly/monthly reports +
retention prune, injectable providers), AnalyticsDeepLink (wenza://analytics
report routing).
Fixed a reconcile bug caught by tests: a retention-boundary week was generated
then immediately pruned, regenerating every run — aligned completedWeekStartKeys
to the prune cutoff → idempotent.
Tests: AnalyticsAdapterTests + AnalyticsCoordinatorTests. Verified standalone via
swiftc (~90 analytics assertions total across engine/store/adapter/coordinator).
Remaining (app wiring, notifications, UI) requires an Xcode build — logged in
ISSUES.md as device-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
KC-49: week/3-day dead space — the outer .frame only allowed fill; force the
timeline ScrollView greedy (.frame maxHeight .infinity) and pin each timeline
root VStack top, so the header pins to the top instead of floating mid-screen.
KC-50: timeline blocks now have a long-press context menu — Complete, Snooze
(15m/1h/tomorrow), and the shared TaskMenuItems (reschedule/move/priority/
category/edit/delete). Events get no menu. CVTimeBlock takes @ViewBuilder menu.
Logs KC-49 (supersedes KC-48) and KC-50 in ISSUES.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The timeline views' content is a thin header + a greedy ScrollView (≈0 ideal
height), so the top-level VStack shrank and the bottomTrailing ZStack floated
it to the bottom — leaving a big empty gap above the day header. Month view
has a tall fixed grid so it never surfaced. Pin the VStack to fill height and
top-align.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the TickTick weekly reference — a toggle flips the week between the
timeline and a grid of day-cards (2 columns), each card listing that day's
schedule as colored bars. Tapping a card opens that day. Both layouts swipe
by week.
- weekGrid state + toggle button (timeline/grid icons)
- weekGridLayout + weekDayCard
(Not build-verified — sim runtime removed to reclaim disk.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The headline of the TickTick weekly timeline — task blocks are now draggable
vertically to change their time, snapped to 15-minute steps. Applies on the
week, 3-day, and day timelines.
- CVTimeItem carries taskID + recurring; task items are tagged, events are not
- CVTimeBlock: a draggable block (GestureState offset while dragging, commits
on release); events and recurring tasks render fixed (non-draggable)
- rescheduleTask: clamps to the visible hour range and writes the new time via
taskVM.setDate on the same day
(Not build-verified — sim runtime removed to reclaim disk.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the mini-month + agenda-list split with a proper weekly timeline —
hours down the left axis, one column per weekday, events as time-positioned
blocks — reusing the same cvTimeGrid/cvDayColumnHeader the day and 3-day
views already use. Horizontal swipe navigates by week (navigateWeek).
Removes the now-unused cvAgendaItems helper.
(Not build-verified — sim runtime removed to reclaim disk; reuses existing
timeline components.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two refinements to get closer to the TickTick reference:
- Drag up on the calendar collapses to the selected week; drag down expands
to the full month (horizontal swipe still changes month). One direction-
aware DragGesture handles both axes.
- The selected day now gets a rounded full-column highlight behind its
number + bars when collapsed (as in the tapped-day screenshot), instead
of only the day circle.
(Not build-verified — sim runtime removed to reclaim disk.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Matches the referenced TickTick monthly view:
- Day cells now show stacked colored schedule BARS (workout/events/tasks,
up to 4) instead of dots — a glanceable overview of the month.
- Selecting a day COLLAPSES the grid to just that week, giving the day's
agenda room below (as in the tapped-day screenshot). A chevron grabber
expands back to the full month.
- DayCell: bars param + stacked RoundedRectangles, top-aligned, height 54
- monthView: renders week rows (all when expanded, only the selected week
when collapsed) via weekRows()/selectedWeekRow(); grabber toggles
- eventDots cap raised to 4 for the bar stack
(Not build-verified — sim runtime removed to reclaim disk.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The top title now tracks whichever year is at the top of the year scroll
(e.g. scroll into 2027 and the header reads '2027'), instead of staying on
the displayed month. In month/week/day modes it still shows month + year.
- visibleYear state driven by a YearTopKey preference reporting each year
block's top offset in the 'yearScroll' coordinate space
- headerTitle switches on viewMode
- iOS 16-safe (GeometryReader + PreferenceKey, no scrollPosition API)
(Not build-verified — sim runtime removed to reclaim disk.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the single paged year (< 2026 >, with dead space below December)
with an infinite vertical scroll where years flow one after another. Opens
anchored on the current year (accent-colored); scroll up for past years,
down for future. Bottom padding clears the floating tab bar / + button.
- yearView: ScrollViewReader + LazyVStack over a year range, scrollTo(current)
- yearBlock(_:): bold year label + 12 mini-months (reuses cvMiniMonth)
- Removed navigateYear + the ◀ year ▶ header (superseded by inline labels)
(Not build-verified — sim runtime removed to reclaim disk; standard SwiftUI
+ existing cvMiniMonth helper.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the single lifetime-tally banner (which used a meaningless streak%7
tick bar) with a 3-page paged TabView:
1. This week — count + 7 per-day ticks for the current week
2. This year — count + 12 month ticks, with all-time total underneath
3. vs last week — this week's count and the delta (▲ more / ▼ fewer / same)
Adds WorkoutViewModel breakdowns: workoutsThisWeek/LastWeek/ThisYear,
currentWeekDays (7 flags), currentYearMonths (12 flags).
(Not build-verified — sim runtime removed to reclaim disk; uses only APIs
already present in the original banner.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Tomorrow/Later rows carry their quadrant color on the left accent bar
and the category chip, so tinting the checkbox too was redundant. Match the
neutral text3 stroke the timeline rows already use.
(Not build-verified — local sim runtime removed to reclaim disk; trivial
one-line swap to an API already used elsewhere in the same file.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .gitea/workflows/ci.yml: build + test KisaniCal scheme on a self-hosted
macOS runner for every PR into main/develop (and pushes to develop).
- .gitea/workflows/release.yml: archive + export IPA on push to main, with
a commented TestFlight upload placeholder (App Store Connect API key).
- ExportOptions.plist: export template (Team ID K8BLMMR883, app-store method).
- scripts/gitea-setup.sh: idempotent Gitea API setup for develop default
branch + main branch protection.
- CONTRIBUTING.md: feature/* -> develop -> main workflow and PR gate rules.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per user model: the workout count should reflect every workout achieved and
never break. Replaces the consecutive/weekly-goal streak with a simple count
of distinct workout days (totalAchievedWorkoutDays). A below-goal week (4/5)
and a full week (5/5) both count fully (=9); a missed day or blank week never
reduces it; duplicate HealthKit days count once.
- ExerciseModels: streakDays = Set(workoutDates).count
- StreakLogicTests: retargeted to tally semantics (4/5+5/5=9, blank-week,
duplicates, skipped-day, empty)
- SettingsView: goal caption no longer claims the streak can be lost
NOTE: not built/tested locally — CLI xcodebuild wedges on actool via the
broken CoreSimulator daemon (missing sim runtimes, machine-wide). Verify in
Xcode GUI (prefs now fixed) or on device.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Workout streak now honors the weekly goal ('reach your goal every week'):
counts workout days across consecutive kept weeks, so rest days and a
skipped day within goal no longer reset it to 1 (KC: profile showed
streak 1 with 5 workouts that week).
- Task 7-day bars / 'this week' now count recurring occurrences
(completedOccurrences), fixing permanently-empty bars alongside a
nonzero done rate.
- New task day-streak stat on the profile Tasks card.
- New ActivityHistoryView: 90-day day-by-day archive of completed tasks
(times, repeat markers), workout logs (sets/volume), missed/made-up days.
- StreakLogicTests: 8 unit tests; algorithm additionally verified via
standalone harness (7/7 scenarios incl. the skipped-Thursday case).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TaskRowView checkbox: 18pt circle -> 16pt rounded square (cornerRadius 5),
matching the requested look. Keeps the quadrant-color stroke and 36x44 tap target.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decoded the device error: NSOSStatusErrorDomain 561017449 = '!pri' =
AVAudioSession insufficientPriority — the mic was denied because a session was
left active (which also blocks the keyboard's own dictation).
Root cause: the early-return guards (recognizer unavailable / invalid format) ran
AFTER setActive(true) but never deactivated, leaking an active session. Also the
.measurement + .duckOthers config is more prone to priority denials.
Fixes:
- Deactivate any stale active session before starting.
- Use .record / .default (least-exclusive recording config).
- stopEngine() now always releases the session (no early-out), and the bailout
guards call it so the session is never left active.
- Map 561017449 to an actionable message (close other audio apps / restart).
Device-specific; not reproducible on the sim (blocks at the permission dialog).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
save() pushed all 7 workout keys to iCloud KVS, but init only restored 4 —
workout history, missed dates, and rest-day compensations were uploaded yet
never restored, so they were lost on reinstall/update. Added the three missing
restoreIfMissing calls so restore mirrors save.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fast-path early-return added in the voice-binding fix
(`if tv.textColor == .label && tv.text == text && editing { return }`) skipped
rebuilding the attributed text while editing — so token highlights never
reapplied as the parser updated highlightRanges on each keystroke. Removed it so
highlights always render. Also switched the highlight tint from systemBlue to the
app accent (orange) to match the established design.
Verified on simulator: "Remind me to pray everyday" → "Remind me to" + "everyday"
highlighted in accent orange; chips Today / Daily.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the most common causes of the engine failing to start:
- Defensive teardown before starting (removeTap + stop + reset) — a leftover tap
from a prior attempt makes installTap throw "already has a tap".
- Validate the input node's hardware format (sampleRate/channels > 0) and fail
with a clear "No microphone input" message instead of throwing.
- Use inputNode.inputFormat(forBus:0) for the tap.
- Category .playAndRecord + [.duckOthers, .defaultToSpeaker] (more permissive
than .record with .duckOthers).
- Surface the underlying NSError domain+code in the banner + log to pinpoint any
remaining device-specific failure.
DEBUG-only KISANI_VOICE_START hook to drive the real engine on a sim.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mic captured speech and the transcript reached rawText, but it never appeared
in the field. NLTaskField.updateUIView (UITextView representable) returned early
when the field wasn't focused (`guard coordinator.isEditing`), so externally-set
text (voice dictation, the normal not-focused case) was dropped and the
placeholder stayed.
Fix: render non-empty `text` even when not editing, placing the caret at the end;
keep the cursor-preserving fast path while actively editing. No new voice system —
this only fixes the existing mic button's transcript→field binding.
Verified on simulator via the real speech.transcript path: "Buy milk tomorrow" →
field shows it (chip: Tomorrow); "Call Romeo at 11 AM" → field shows it (chip:
Today 11:00). Adds DEBUG-only test hooks KISANI_AUTOADD / KISANI_VOICE_SIM
(compiled out of Release). Logged under KC-38.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parseRecurrence required a space ("every day"), so "everyday" (one word) was
missed. Now \bevery\s*day\b matches both, plus "each day". Added 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. 6 parser tests added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8 tests covering daily / weekly / every-weekday / monthly / yearly expansion,
repeat-until cutoff, time-of-day preservation on the mapped local day, and
non-recurring single-day placement. Uses Calendar.current so it holds in any
timezone. Closes the recurrence-coverage gap from the calendar fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mic icon in AddTaskSheet was a static Image with no action and no
speech service behind it. Full implementation:
SpeechRecognizer.swift (new):
- @MainActor ObservableObject wrapping SFSpeechRecognizer + AVAudioEngine
- Requests speech recognition then microphone permission in sequence
- Streams partial results into @Published transcript; final result stops engine
- Suppresses transient error 301 (no speech detected)
- reset() cleans up on sheet dismiss
- [VOICE] log trail at every step: tapped → granted → started → transcript → saved
TodayView.swift — AddTaskSheet:
- @StateObject private var speech = SpeechRecognizer()
- onChange(speech.transcript) writes transcript to rawText (task field)
- Mic Image → Button { speech.toggle() } with mic.fill + pulse animation while recording
- .safeAreaInset shows a red dismissible error banner on permission denial
- submit() stops recording before saving; logs task title
- .onDisappear resets speech state
project.yml:
- NSMicrophoneUsageDescription
- NSSpeechRecognitionUsageDescription
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
Root cause: a data-integrity bug, not visual. The month 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 (or when the user sets
"Start Week On" = Monday) every column was mislabeled by one and the W-number
landed on the wrong column — e.g. June 1 2026 (a Monday) appearing under "S".
Fix:
- Extract pure, testable date math into CalendarGrid (monthGrid + weekdaySymbols).
- Header now derived from firstWeekday, so it always matches the grid.
- Week-number label gated on isWeekStart (weekday == firstWeekday), not Sunday.
- Document that week numbers are locale weekOfYear (consistent with the layout),
not ISO.
- gridDays() and the year-view mini-month dedupe to CalendarGrid.
Verification:
- New KisaniCalTests target with 9 tests, all passing on simulator: exact
Sunday-first June 2026 grid, header↔grid alignment for firstWeekday 1...7,
cell date identity, month navigation (May/Jul/Feb 2026 + Feb 2028 leap +
Dec 2026→Jan 2027), and event-day bucketing (all-day, late-night, midnight-
crossing) across 4 timezones.
- Verified in the running app: header S M T W T F S, May 31 in the Sunday
column, June 1 under Monday, June 24 (Wednesday) selected.
Adds a DEBUG-only KISANI_INITIAL_TAB launch env (compiled out of Release) used
to screenshot the calendar past the sign-in gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previous logic compared dueDate against startOfDay for all tasks, so a task
due at 2:00 PM stayed in "Today" until midnight even after the time passed.
TickTick rule:
- hasTime=true → overdue the moment dueDate < now (wall clock)
- hasTime=false → overdue at start of next day (dueDate < startOfDay)
overdueTasks: now uses `dueDate < now` for timed tasks, `dueDate < today`
for date-only tasks.
todayTasks: now excludes timed tasks whose time has already passed — those
belong in overdue, not today — while keeping date-only tasks in Today for
the full calendar day.
Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
- Remove 🎉 from the My Tasks widget empty state.
- Remove 𝕏 👾📸 from Settings → Follow Us row.
- About row and About sheet now read the version from the bundle
(new Bundle.marketingVersion / buildNumber / versionDisplay helpers)
instead of a hardcoded "v1.0.0", so it always tracks the real build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>