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>
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>
- .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>
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>
Calendar timeline views only populated in Month. Fixes:
- Day/3-Day time grid dropped untimed (midnight) tasks and ignored
recurrence. Now uses occurrences(on:) + hasTime, and adds an all-day
band for untimed tasks, all-day events, and the day's workout.
- Week showed abstract color bars; now lists real task/event titles
(up to 4/day + "N more") via a shared cvAgendaItems helper.
- List view and the task-list toggle now use occurrences(on:) so
recurring tasks appear.
TodayView:
- Slimmer task checkbox (18pt open ring vs 22pt filled donut).
- Confirm before completing a future-dated task (Next 7 Days / Later)
to prevent accidental completion; today/overdue still toggle instantly.
Bump build to 2.0 (9). Add App Store support-site one-pager.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Bump LastUpgradeCheck/scheme to Xcode 2620
- Enable DEAD_CODE_STRIPPING, ENABLE_USER_SCRIPT_SANDBOXING, and
STRING_CATALOG_GENERATE_SYMBOLS across targets
- Set CURRENT_PROJECT_VERSION to 8
- Lock iPhone orientation to portrait; normalize iPad orientations
- Use explicitFileType for app/appex product references
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a Customize → "Time Milestones" toggle bound to the App-Group
kisani.periodMilestones flag (so NotificationManager reads the same value).
Flipping it reschedules immediately — off clears the day/week/month/year
notifications, on re-schedules them. Defaults on. Closes the pending #2 from the
KC-31 audit; #6 (configurable thresholds/snooze) remains pending.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Lock-screen Mark Complete / Snooze now push to iCloud (CloudSyncManager) and
reload widget timelines, so background completions reach iCloud/Watch/widgets
immediately instead of waiting for the next app run.
- Notification copy ("Urgent — tap to mark complete" + evening check-in) now uses
the live computed Matrix quadrant: reschedule snapshots urgent task IDs from
displayQuadrant and threads them into scheduleTasks/notifBody, instead of the
stale stored task.quadrant.
- postponeAllOverdue now mirrors single postpone (clears urgencyOverride and
re-syncs the stored quadrant per task).
- ISSUES.md: added KC-31 (audit), corrected KC-30 (midnight day/week/month/year);
left #2 (period-notif Settings toggle) and #6 (configurable thresholds/snooze)
as pending; noted #1 (Move lists 3 quadrants) as intentional KC-25 behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WenzaWatch (new watchOS target, embed disabled until the watchOS platform is
installed — see project.yml note):
- Today list of actionable tasks (dated today/overdue + recurring occurrences),
tap-to-complete with haptics, and add-via-dictation.
- Reads/writes the same tasks through the shared iCloud KV store that
CloudSyncManager mirrors on the phone; watch entitlement uses the iOS app's
kvstore identifier so they share one store. New tasks include all required
TaskItem keys so the phone decodes them.
- NOTE: not compiled here (watchOS SDK absent on this machine); iOS build
verified green with the embed commented out.
Period notifications: day/week/month/year now all fire at midnight (00:00),
adding the daily "One more day passed." (Pretty Progress style).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Workout window: the header ⋯ menu gains "Mark All Complete" and "Clear All
Sets" (Manage Workouts preserved). New WorkoutViewModel.setAllSetsDone(_:)
ticks every set across all sections/exercises and runs the normal completion/
logging path.
- Recurring tasks: completing an occurrence now posts a quiet "✓ Done. Repeats
<tomorrow / in 1 week / …>" confirmation via NotificationManager
.notifyRecurringCompleted, phrased from the next active occurrence. Hooked
into TaskViewModel.toggleOccurrence; un-completing does nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Bars widget: tracked ("Track Countdown") events now also advance — a completed
or past (non-recurring) tracked event falls through to the next-nearest, and the
refresh boundary covers the tracked event's expiry. Recurring tracked events
still roll forward. Completes the KC-26 auto-advance for all paths.
- Progress Dots on the lock screen (accessoryRectangular): show 28 larger dots
instead of 100 tiny ones, render with .primary for the vibrant tint, and use
AccessoryWidgetBackground so it's legible (Home Screen keeps the 100-dot grid).
- Notifications: snooze action labels now read "Snooze 15 min/30 min/1 hour/
2 hours" (lock screen) and "Snooze 15 Minutes…" (in-app Postpone menu).
- New period milestone notifications: recurring "One more week/month/year passed"
at end of week (Sun 20:00), month (1st 09:00), and year (Jan 1 09:00); gated by
a kisani.periodMilestones flag, scheduled via the existing reschedule path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Today ⋯ menu: implemented the placeholder actions.
- Background: 5 selectable themes (Default/Warm/Cool/Mono/Paper), persisted.
- Group & Sort: group by Date (default)/Priority/Category/Quadrant and sort by
Smart/Due/Priority/Title; non-date groupings render via UpcomingSection.
- Select: multi-select sheet with Select All/Deselect All and bulk
Complete/Priority/Move/Delete.
New isolated file TodayMenuFeatures.swift; added TaskViewModel.allActiveRows.
- Matrix "Move" submenu now lists only the other three quadrants (excludes the
current one) using clear matrix labels.
- Event Countdown (Bars) widget auto-advances: each timeline entry re-resolves
"nearest to finish" as of its own time and refreshes at the moment the current
event expires, so it rolls to the next event on expiry/completion.
- Splash wordmark rebranded to "wenza." (regular-weight typewriter + orange dot).
- ISSUES.md: logged KC-23 (Pick Date + time-based reminders), KC-24 (Wenza
rebrand), KC-25 (Move trim), KC-26 (widget auto-advance).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Renames the user-facing app to Wenza without changing identity:
- CFBundleDisplayName → "Wenza" (app) and "Wenza Widgets" (widget extension).
- Calendar/Health permission prompts now reference Wenza.
- In-app copy updated: sidebar title, Auth + Onboarding screens, Settings
about/help rows, feedback email subject, tutorial text, widget description.
Bundle identifier, App Group, UserDefaults storage keys, team ID, and the
internal Xcode target/project names are intentionally unchanged, so this stays
the same App Store app with no user data loss. App Store listing name must be
changed separately in App Store Connect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
Refines the Eisenhower Matrix toward TickTick's "view over attributes" model:
- Split the single quadrant override into two axes: importance is always
Priority (High = top row); urgency is date-derived unless manually pinned
via the new `urgencyOverride` flag. Horizontal drags pin urgency instead of
fabricating/destroying a real due date; vertical drags set Priority directly.
- A drag only flags "Manual" when the target column disagrees with the
date-derived urgency, so dropping into a task's natural column stays on Auto.
- Tiles show a "Manual" badge; the task menu gains "Reset Matrix Urgency".
- Urgency window is now 3 days by default, widened to 14 for prep-heavy event
types (birthday/domain/annual + subscription/renewal/exam/deadline titles).
- New tasks no longer default to today — no date stays no date (not urgent).
- Stored `quadrant` is kept synced to the computed placement so task color and
widget accents follow the Matrix; Settings stats count by displayQuadrant.
- Completed tasks are hidden from the Matrix grid by default (toggle to show).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Priority defaults only fired at creation, so tasks saved before Priority
existed (e.g. birthdays) all had `.none` and fell to the Matrix bottom row
instead of Q2. Added a one-time, flag-guarded migration on load that applies
the KisaniCal category defaults (birthday/domain/annual/exam/subscription →
High, workout → Medium) to existing `.none`-priority tasks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Matrix (KC-21): the Eisenhower Matrix is now a computed view of tasks.
Importance = Priority (High = top row), urgency = deadline proximity, so
tasks auto-place into Q1–Q4. Added a persisted `matrixOverride` so a manual
drag pins the task and syncs its Priority to that row (top → High, moving
down demotes High → Medium) and never gets forced back; editing Priority,
due date, or the editor clears the override to re-place live. New tasks get
KisaniCal priority defaults (birthday/domain/annual/exam/subscription → High,
workout → Medium). AddTaskSheet drops the manual quadrant picker for a
Priority menu (pick Priority + Date only).
Also includes in-progress Progress Dots widget work (day/week/month/custom
event modes, App-Group anchor, recurrence-aware spans).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The real bug behind "Leona's Birthday, 5 days left" showing 0% filled:
Mode B (recurring span: previous -> next occurrence) only ran for
app-tracked events, while the upcoming-queue / picked-event paths still
anchored from first-seen (so a yearly birthday started at 0%).
- EventEntity now carries isRecurring/recurrenceLabel (populated from the
shared task data) so the auto-select and picker paths know the rule.
- resolveSpan() centralizes the window: recurring -> prev/next occurrence
everywhere; one-off -> explicit start / tracking date / first-seen.
- A yearly birthday 5 days away now reads ~98% (≈360/365 days elapsed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Task long-press menu gains Track/Stop Tracking Countdown, storing the
tracked event id + tracking date in the App Group (CountdownTracker)
and reloading widget timelines.
- The widget reads the tracked event directly and bypasses its config:
Mode A (one-off) = tracking date -> event date; Mode B (recurring) =
previous occurrence -> next occurrence via the task's recurrence rule.
- WidgetTask decodes isRecurring/recurrenceLabel from the shared JSON.
- With nothing tracked, the configured modes remain the fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>