Compare commits

..

59 Commits

Author SHA1 Message Date
kutesir
36986ebe00 docs: log KC-72 (StreakLogicTests fix + runner recovery)
Some checks failed
Release / archive-and-export (push) Failing after 1m9s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
44b4d20b3c test: mark StreakLogicTests @MainActor to fix Swift 6 compile error (KC-72)
The CI runner was broken all session (registered but never actually
polled for jobs - separate infra bug, fixed alongside this). First
real CI run surfaced a genuine, pre-existing compile error unrelated
to tonight's work: StreakLogicTests called @MainActor-isolated
WorkoutViewModel/TaskViewModel static methods from a non-isolated
test class. RecurrenceTests.swift in the same target already uses
the correct @MainActor class annotation - StreakLogicTests was just
missing it.

Verified locally: full suite now passes, 78/78 tests, 0 failures,
on a real simulator (iPhone 17 Pro) - not just self-review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
725c4a0324 ci: finish release.yml's TestFlight upload, rule out Xcode Cloud for this repo (KC-71)
Spent considerable effort trying to connect Xcode Cloud directly to
this repo's self-hosted Gitea - confirmed not achievable. GitHub
Enterprise's provider option hits Gitea's missing /api/v3/ path;
GitLab Self-Managed rejects Gitea's UUID-format OAuth2 Client IDs
against App Store Connect's 64-char-hex requirement (verified by
actually creating a real OAuth2 app and having it rejected). Also
stood up Tailscale Funnel on the Gitea host for real public HTTPS
access regardless (was LAN-only before), and switched origin to it.

Replaced release.yml's commented-out altool placeholder (defunct -
Apple retired that upload path in 2023) with the current supported
approach: xcodebuild -exportArchive with API-key auth flags handles
export and TestFlight upload in a single step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
b7af2dd76b tasks: add "every N weeks/months/days/years" recurrence (KC-70)
Recurrence previously only supported 5 fixed rules with no interval.
Adds TaskItem.recurrenceInterval (optional, nil = 1, so every existing
task is unaffected), a stepper in the shared create/edit sheet, quick-add
NLP for phrases like "every 2 weeks"/"biweekly"/"fortnightly", and
threads the interval through both countdown widgets so their progress
bars size correctly for biweekly/etc tasks too.

Built on KC-69's consolidated RecurrenceRule engine. Verified two things
by actually compiling and running standalone swiftc drivers rather than
just reading: the core interval math (44/44, unchanged from KC-69) and
the new quick-add regex patterns against realistic phrases including
"look into tru housing options every two weeks" (11/11 passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
f6a9358a86 recurrence: consolidate 3 duplicate implementations into one shared, tested engine (KC-69)
Before adding "every N weeks/months" support, found the recurrence
window math was independently hand-written in three places (main app's
TaskItem.isOccurrence, and two separate widget extensions' countdown
progress calculations) with no shared code to keep them in sync.

Extracted the math into Shared/RecurrenceRule.swift (pure Foundation,
no framework deps) compiled into both the app and widget extension
targets via a new project.yml Shared/ source path, and pointed all
three call sites at it. Verified every case is identical to prior
behavior at interval==1 by compiling and running a standalone test
driver directly against the real file with swiftc - 38/38 passing,
including Jan-31-monthly and Feb-29-leap-year-yearly edge cases.

Groundwork only - no recurrenceInterval field on TaskItem yet, the
actual interval feature comes next on top of this.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
ee4c33a3c3 workout: restore confirmed Undo for manually-logged days only (KC-68)
KC-67 removed Undo entirely on the reasoning that Watch/Health is the
source of truth once a day is logged - correct for a Watch-detected
day, but it also blocked undoing a day logged manually via the
in-app Finish button, leaving no way back (hit while testing KC-67
itself). Added manualWorkoutDates to track which completions came
from the app's own Finish button vs HealthKit sync, and only offer
Undo (now confirmed via a dialog) for those.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
3a35783331 workout: fix iCloud sync stomping the Watch-logged streak, remove Undo (KC-67)
Root cause: CloudSyncManager.kvStoreChanged blindly overwrote any
externally-changed iCloud key, including workoutDates - a lifetime
tally that every local write path (HealthKit sync, manual finish)
carefully dedups/unions. A stale iCloud snapshot racing a fresh
Watch-sync write could silently replace the correct array with an
older, smaller one. Now unions instead of overwriting for that key
specifically.

Also: removed the Undo action/row from DotProgressCard entirely (Watch/
Health is the source of truth once a day is logged), and restored the
confirmation dialog on Finish Workout so every state-changing action
confirms first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
239eb4845f liveactivity+widgets: fix iOS 16.2 build error, extend createdAt to Bars widget (KC-66)
end(_:dismissalPolicy:) requires iOS 16.2 but LiveActivityManager.end
only gated to 16.1 (Live Activities' own minimum) - branch on
#available with the deprecated overload as fallback. Separately,
auditing all countdown widgets per user request found the "Event
Countdown (Bars)" widget family had its own independent start-date
resolution that never considered the task's real creation date at
all - EventEntity now carries createdAt and resolveSpan prefers it
the same way KC-65 already does for the dot-grid widget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
a73f313fde model: give TaskItem a real createdAt so countdown widgets stop resetting (KC-65)
The Track Countdown widget already preferred task.createdAt as its
progress start date, but TaskItem never had that field — it was
always nil, so every task fell back to a fragile "first render"
anchor that can reset to now. New tasks now record a real creation
timestamp; old tasks keep the existing fallback since there's no way
to know when they were actually made.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
d5a8faf83e liveactivity: fix deprecated end(using:dismissalPolicy:) warning (KC-64)
Omitting the content argument on activity.end(dismissalPolicy:)
resolved to the deprecated iOS 16.1 overload. Pass nil explicitly to
bind to end(content:dismissalPolicy:) instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
d429dd04dc workout: revert Finish-at-100% confirmation, keep dots-always-visible fix (KC-63 follow-up)
Confirming every Finish tap was unwanted friction once all sets are
already checked. Restored the one-tap Finish at 100% (partial
completion still confirms via "Finish Anyway"); the always-visible
progress dots from the first part of KC-63 are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
4b006cfd39 workout: keep progress dots visible after finish, confirm every Finish tap (KC-63)
The KC-52 redesign swapped the whole progress-dots view for a green
"Workout logged" takeover on finish, with no way back short of Undo,
and let Finish skip confirmation whenever all sets were already
checked. Dots now stay visible in every state; Finish always confirms,
matching Clear All / Didn't Train.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
172bb011ff tasks: typography polish on task rows, no size changes (KC-62 follow-up 2)
Title weight bumped to medium for hierarchy, tracking added to the
mono date/countdown lines for legibility at small size, slightly
looser line spacing. Impeccable's detect engine only parses HTML, not
Swift, so this was done by hand rather than via the skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
92a533a930 tasks: rebalance row layout, countdown+icons move to right column (KC-62 follow-up)
The category icon column sat empty for most rows (default tasks have
no glyph/alarm/series), leaving a lone repeat icon floating in dead
space on the right per user's device screenshot. Move the countdown
label out from under the title into a trailing column paired with the
indicator icons, so both sides of the row carry equal weight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
80fbbd7895 tasks: replace category text pill with TickTick-style icon glyphs (KC-62)
Every task row was tagging itself "Reminder" via a text badge for the
default category, adding no information. Swapped for icon-only glyphs
(nil for the generic .reminder case) folded into the existing alarm/
repeat indicator row; countdown/date info was already present or
redundant with the leading time-track, so no separate countdown text
was added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
cddbefd314 onboarding+liveactivity+habits: 3 fixes from device feedback (KC-61)
1. Onboarding Habits caption now explicitly mentions the toggles are
   changeable in Settings, not just times/custom reminder.

2. TaskLiveActivity: removed the checkbox-look circle glyph (hourglass
   instead). iOS has no API for an app to block the system's swipe/dismiss
   gesture on a Live Activity -- not attempted. Added a real Stop control via
   a new StopCountdownIntent (LiveActivityIntent, iOS 17+) embedded directly
   in the card (lock screen '×' + Dynamic Island 'Stop Countdown' button) --
   the actual iOS-supported equivalent of 'long press for options', since
   Live Activities don't support notification-style long-press menus.

3. Habit notifications gain 'Not Today' (dismiss only, intentional no-op --
   the lifetime-tally philosophy has no concept of a logged miss) and 'Stop
   Tracking' (destructive-styled; flips the habit's master toggle off and
   cancels every pending notification for that type across all its slots).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
fc6ecc4979 tutorial: introduce habit reminders in the Settings tutorial too (KC-60)
Habit reminders (KC-55) were in Onboarding but not the app's separate
spotlight/coachmark tutorial system -- skipping onboarding meant no second
chance to discover Prayer/Bed/Coffee reminders exist.

Adds a fourth TutorialManager track (settingsTips/settingsStep/settingsDone),
matching the existing Today/Workout tip-track pattern exactly, and wires it
into SettingsView via the same shared InViewTutorialCard component -- shown
once, first time Settings opens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
d3e781c8e7 tasks: add 'Complete & Stop Series' for recurring tasks (KC-59)
Recurring tasks only had Delete (erases the task + its entire completion
history) as a way to stop a series -- no in-between option to wind one down
while keeping its record.

TaskViewModel.completeAndStopSeries(_🔛) marks the occurrence complete and
sets recurrenceEnd to that day -- a field that already existed in the model
and was already respected by isOccurrence, just had no UI. Task, title, and
full completedOccurrences history stay intact; only future occurrences stop.

New menu item in the shared TaskMenuItems (Today/Matrix/Calendar), shown only
for recurring tasks. Wired through all 8 call sites, several via a new
optional closure threaded through intermediate wrapper views (QuadrantCard,
DayTimelineView, OverdueCard, UpcomingSection).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
e0bc5538c1 Bump build to 2.0 (10)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
ec58032246 habits: clarify the custom coffee reminder row (KC-58)
The custom-slot row just said 'Coffee' -- identical-looking to the card's own
'Coffee reminders' title, with no affordance signaling it's an editable,
user-nameable 5th slot (distinct from the 4 fixed Morning/Midday/Afternoon/
Evening ones).

Added a small pencil + 'YOUR OWN REMINDER' label above the field; changed the
default from pre-filled 'Coffee' to empty so the placeholder ('Name it, e.g.
"Second coffee"') actually does its job. NotificationManager falls back to
'Coffee' for the notification text only if the user never typed a name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
a6e21e3b52 fix(theme): add AppColors.coffeeBrown -- Color(r:g:b:) is file-private (KC-57)
Build error: Color(r:g:b:) is a private extension scoped to DesignTokens.swift
(only UIColor's sibling init is also private, both file-scoped by design).
KC-55/56 called it directly from SettingsView.swift and OnboardingView.swift
for the coffee icon color -- invisible there, so the compiler fell back to
unrelated overloads ('Cannot convert Int to Color', 'Extra argument b').

Added AppColors.coffeeBrown (defined where the private init is visible),
replacing both inline Color(r:g:b:) call sites -- matches how every other
color in the app is centralized and accessible everywhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
6fe896083f habits: add a 'Done' notification action + private day-count badge (KC-56)
Follow-up to KC-55's design question: habit reminders (Prayer/Bed/Coffee) stay
out of Today/Matrix/Activity History (would add 3-9 daily checkboxes forever,
against 'nothing shouts'), but now get a 'Done' action on the notification
itself -- like the workout confirm flow's 'Yes, log it' -- that logs a private
timestamp.

New HABIT_LOG category/HABIT_DONE action; every habit notification carries
userInfo['habitType'] (bed/prayer/coffee -- slot detail collapses to the type).
logHabitDone/habitDoneCount store a deduped date array in UserDefaults.kisani,
mirroring the lifetime-tally streak philosophy from KC-40 rather than a
breakable streak. Settings shows a quiet 'Xd' badge per habit -- the only
place the count appears; nothing touches the task list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
39c95784db habits: add Prayer, Bed-making, and Coffee/Caffeine reminders (KC-55)
Three new opt-in daily local-notification habits, toggleable in both Settings
and Onboarding, backed by the same UserDefaults.kisani (App Group) keys:

- Prayer: up to 4 independently-toggleable times/day (Morning/Afternoon/
  Evening/Night), 'Have you had a chance to pray and give thanks?'
- Bed-making: one daily nudge, default 8am (research-backed first win of the
  day per the user's note).
- Coffee/Caffeine: up to 4 named times + one custom-labeled reminder.

NotificationManager.scheduleHabitReminders() schedules/cancels all slots
idempotently via fixed identifiers, wired into the existing reschedule()
orchestration. Onboarding gets 3 restrained master toggles with sensible
defaults (no per-slot editors, to avoid a long form); Settings gets the full
Habit Reminders sheet with per-slot times and the custom coffee reminder.

Found and worked around a pre-existing store mismatch: workoutCheckInEnabled/
workoutConfirmEnabled etc. write to UserDefaults.standard (no explicit store:)
but NotificationManager reads them from UserDefaults.kisani -- not fixed here
(out of scope), but all new keys explicitly use store: UserDefaults.kisani so
this feature actually fires.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
dc5c403a36 tasks: fix timed recurring tasks vanishing from Today after their time passes (KC-54)
Real bug: todayTasks excludes timed tasks whose time has passed ('belongs in
overdue'), but overdueTasks deliberately excludes ALL recurring tasks (it feeds
postpone/postponeAllOverdue, which mutate dueDate by id -- for a recurring task
that's the recurrence anchor, not just today's occurrence; including them there
would let Postpone All corrupt the whole series). Recurring tasks fell into the
gap between the two rules and vanished from the Today tab entirely once their
time passed, while Calendar's day view (no time-of-day gating) kept showing
them fine.

Fix: todayTasks only excludes past-time NON-recurring tasks. Recurring
occurrences always stay in Today, matching Calendar, without ever touching the
overdue/postpone path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
7bb37d1995 widgets: add 'This Year' mode to Progress Dots (KC-53)
The Progress Dots lock-screen widget already supports Day/Week/Month via its
configuration picker — no code needed for those. Year was the only mode
missing. Added ProgressDotMode.year, a matching provider branch
(Calendar.dateInterval(of: .year)), and yearTitle(for:), mirroring the
existing day/week/month pattern exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
709eee2579 workout: stop auto-completing the day when sets are ticked (KC-52 follow-up 3)
Real bug, not cosmetic: setAllSetsDone/toggleSet/setExerciseDone still
auto-called logWorkoutCompleted() whenever doneSets == totalSets — leftover
pre-redesign behavior that fought the new completion card. Ticking the last
box (or tapping 'Check all sets') silently counted the day and snapped the UI
to the green 'done' takeover without the user ever hitting Finish.

Removed the auto-log from all three set-mutation methods. Only the card's
explicit Finish Workout/Finish Anyway (-> markWorkoutDone) now counts a day as
done. Check all sets just checks boxes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
65c93769c5 workout: drop seal icon, confirm every destructive quick action (KC-52 follow-up 2)
checkmark.seal.fill (unused elsewhere, reads as a generic 'AI verified badge')
replaced with checkmark.circle.fill, the app's actual convention. All four
state-changing card actions (Finish Anyway, Check All Sets, Clear All Sets,
Didn't Train) now confirm via one PendingAction enum + confirmationDialog
before executing, each with a tailored message; destructive ones use the
.destructive role. The header menu's Clear All Sets (any completion level)
also now confirms instead of executing instantly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
76366a0065 workout: restrained button, finish confirmation, clear toggle (KC-52 follow-up)
Addresses feedback on the completion card: the primary button was a
permanently-filled accent pill (violates 'nothing shouts'/'color earns its
place') and Finish Anyway skipped confirming before overriding real progress.

- QuietAccentButtonStyle: outlined at rest, fills solid accent only while
  pressed.
- confirmationDialog on Finish Anyway showing the logged % (skipped at 100%).
- Check all sets flips to Clear all sets once fully checked — undo is the
  button itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
4b2463b017 workout: bring completion controls onto the main page (KC-52)
DotProgressCard becomes a 3-state completion card (pending/done/missed) with a
primary Finish Workout/Finish Anyway action that captures today's workout
honestly — regardless of how many sets got ticked, since Health/Watch already
confirms the workout independently. Secondary actions: check all sets, mark
not completed. Done/missed states show an Undo.

Adds WorkoutViewModel.unmarkWorkoutDone/unmarkWorkoutMissed. Removes 'Mark All
Complete' from the header '...' menu (superseded by the card); keeps 'Clear
All Sets' as a distinct reset utility.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
c75a067993 fix(healthkit): use let for interval captured across async let tasks
Build error (Xcode, Swift 6): 'interval' was a var built via two statements
then captured by 3 concurrent async let tasks in dailyReference — Swift 6
requires captured values to be immutable. It was never mutated after setup;
switched to a single-expression let (DateComponents(day: 1)).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
764e1c2f3d fix(analytics): mark WorkoutSnapshot.init(vm:) @MainActor
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>
2026-07-12 22:57:13 +00:00
kutesir
b4813272f6 analytics(healthkit): wire historical HealthKit reference into reports (KC-51 ph4e)
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>
2026-07-12 22:57:13 +00:00
kutesir
2930557142 analytics(service): capture snapshot value in coordinator closures (concurrency hardening)
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>
2026-07-12 22:57:13 +00:00
kutesir
bcbcda5ba5 analytics(deeplink): route report notification tap to its report (KC-51 ph4c)
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>
2026-07-12 22:57:13 +00:00
kutesir
f8a1f90f8c analytics(ui): AnalyticsView (6 areas + states + charts) + Workout entry (KC-51 ph4b)
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>
2026-07-12 22:57:13 +00:00
kutesir
3f46369078 analytics(app): AnalyticsService + lifecycle reconcile + deep-linked notifications (KC-51 ph4a)
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>
2026-07-12 22:57:13 +00:00
kutesir
f3213c080e analytics(integration): adapter + reconcile coordinator + deep links (KC-51 ph3)
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>
2026-07-12 22:57:13 +00:00
kutesir
8e73615e8b analytics(persistence): file-based store + reconciliation selection (KC-51 ph2)
Phase 2: AnalyticsStore (JSON files under Application Support) with immutable
frozen daily snapshots + immutable, idempotent, deduped weekly/monthly reports,
and 12-month retention pruning. Engine gains completedWeekStartKeys/
completedMonthKeys/missingKeys for idempotent reconciliation selection.

AnalyticsStoreTests: 6 XCTest cases. Verified standalone via swiftc — 23-
assertion temp-dir harness all pass. Registered files via xcodegen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
31cf38e911 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-12 22:57:13 +00:00
kutesir
a4abbb0d4d 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-12 22:57:13 +00:00
kutesir
508d321619 Calendar: force timeline fill (dead space) + long-press menu on blocks
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>
2026-07-12 22:57:13 +00:00
kutesir
e8065820b4 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-12 22:57:13 +00:00
kutesir
51c716ae97 Calendar: fix week/3-day dead space (pin content to top)
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>
2026-07-12 22:57:13 +00:00
kutesir
a34afabb2f Revert "Calendar month view: TickTick-style bars + collapse-to-week"
This reverts commit 2ec3278632.
2026-07-12 22:57:13 +00:00
kutesir
d32e55fd44 Revert "Calendar month: drag-to-collapse + full-column selection highlight"
This reverts commit bd43eecfbe.
2026-07-12 22:57:13 +00:00
kutesir
6e20d665fd Calendar week view: timeline ⇄ grid layout toggle
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>
2026-07-12 22:57:13 +00:00
kutesir
39f4c0fcfa Calendar timeline: drag a task block to reschedule its time
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>
2026-07-12 22:57:13 +00:00
kutesir
7fdf50c898 Calendar week view: 7-day timeline (TickTick-style)
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>
2026-07-12 22:57:13 +00:00
kutesir
2441efe304 Calendar month: drag-to-collapse + full-column selection highlight
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>
2026-07-12 22:57:13 +00:00
kutesir
8f89d3c73b Calendar month view: TickTick-style bars + collapse-to-week
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>
2026-07-12 22:57:13 +00:00
kutesir
23fac88b1d Calendar: header follows the scrolled-to year in continuous year view
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>
2026-07-12 22:57:13 +00:00
kutesir
83d1f1d542 Calendar year view: continuous TickTick-style multi-year scroll
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>
2026-07-12 22:57:13 +00:00
kutesir
a69b54ff19 Workout streak banner: swipeable week / year / vs-last-week pages
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>
2026-07-12 22:57:13 +00:00
kutesir
97cc5589cc Today list: neutral checkbox stroke (color already on accent bar + chip)
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>
2026-07-12 22:57:13 +00:00
kutesir
e79cced64a docs: add CICD.md with pipeline flow diagram and gate rules
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
kutesir
b9d2ca104e docs: add STRUCTURE.md describing targets, layout, and architecture
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:57:13 +00:00
cd2dc1cee6 ci: lowercase runner label (runner registered as self-hosted,macos) 2026-07-12 22:57:13 +00:00
f7bc0b9b3c ci: lowercase runner label (runner registered as self-hosted,macos) 2026-07-12 22:57:13 +00:00
kutesir
cf5dc8943a ci: add Gitea Actions CI/CD, branch workflow, and contributor docs
- .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>
2026-07-12 22:57:13 +00:00

Diff Content Not Available