Merge develop -> main: full session (KC-51 through KC-71) #28

Merged
kutesir merged 64 commits from develop into main 2026-07-12 22:57:14 +00:00
3 changed files with 79 additions and 8 deletions
Showing only changes of commit 3512d55503 - Show all commits

View File

@@ -2292,3 +2292,59 @@ parens (one pre-existing stray `(` predates this change, confirmed earlier
in KC-62). Existing tasks (created before this fix) still have no real
creation date and will keep using the anchor/inferred fallback — there's
no way to retroactively know when they were actually created.
## KC-66 — Live Activity build error + second countdown widget missing createdAt
Status: Implemented (not build-verified — no iOS runtime here)
Reported by: User, Xcode error screenshot + follow-up request to audit all
widgets for creation-date usage
Area: Live Activities, Widgets
### 1. Build error: `end(_:dismissalPolicy:)` needs iOS 16.2, file targets 16.1
KC-64's fix picked the non-deprecated ActivityKit overload, but
`LiveActivityManager.end(taskID:)` is only gated `@available(iOS 16.1, *)`
— matching Live Activities' own minimum — so the call didn't compile for
16.1.x. Branched on `#available(iOS 16.2, *)`: the new overload when
available, falling back to the deprecated `end(using:dismissalPolicy:)`
below that. `StopCountdownIntent.swift` needed no change — it's already
gated `@available(iOS 17.0, *)`, well above 16.2.
Files: `KisaniCal/Managers/LiveActivityManager.swift`.
### 2. Audit: does every countdown widget use the task's real creation date?
KC-65 fixed `KisaniCalWidgets.swift`'s `ProgressDotProvider` (the "Track
Countdown" dot-grid widget, driven by App Group task data) to prefer
`task.createdAt`. Auditing the rest turned up a second, separate countdown
widget — `EventCountdownWidget.swift`'s `EventBarsWidget`/`EventProvider`
(the "Event Countdown (Bars)" widget family, config-driven via
`EventConfigIntent`/`EventEntity`) — whose `resolveSpan` had its own
independent start-date resolution that never looked at `createdAt` at all:
explicit start (tracked-since date, or the widget's configured "Counting
from") → `storedAnchor` (first-seen fallback) only. Confirmed via `grep
createdAt EventCountdownWidget.swift` returning nothing before this fix.
`WidgetViews.swift`'s progress-rendering views (`ProgressDotView`,
`BarGrid`, etc.) only consume an already-computed `progress` value, so
they needed no change — the two `TimelineProvider`s are the only places
that resolve a start date, and both are now fixed.
### Change
- Added `createdAt: Date? = nil` to `EventEntity` (the App-Group-backed
event picker's entity type), populated in both places that build it:
`EventQuery.allEvents()` and `EventProvider.upcomingEvents(asOf:)`.
- `resolveSpan(...)` gained a `taskCreatedAt: Date? = nil` parameter,
checked after `explicitStart` (an explicit user choice, e.g. the
widget's "Counting from" field, still wins) and before `storedAnchor`
(the last-resort fallback for tasks created before KC-65, or fully
custom events with no task behind them at all).
- All three call sites that resolve a real task's span (tracked event,
upcoming-queue auto-select, manually configured event) now pass
`taskCreatedAt:`. The fully-custom-event branch (no task, just a typed
name + date) is unaffected — there's no task to have a creation date.
Files: `KisaniCalWidgets/EventCountdownWidget.swift`.
### Note
Not build-verified (no iOS runtime here). Self-reviewed: balanced braces/
parens confirmed per-file. Same caveat as KC-65: tasks created before that
fix still have no real `createdAt` and fall through to the anchor/inferred
heuristics in both widgets.

View File

@@ -61,7 +61,14 @@ enum LiveActivityManager {
Task {
for activity in Activity<TaskActivityAttributes>.activities
where activity.attributes.taskID == taskID {
// end(_:dismissalPolicy:) needs iOS 16.2; this file supports
// 16.1 (Live Activities' own minimum), so fall back to the
// deprecated using:dismissalPolicy: overload below that.
if #available(iOS 16.2, *) {
await activity.end(nil, dismissalPolicy: .immediate)
} else {
await activity.end(using: nil, dismissalPolicy: .immediate)
}
}
}
}

View File

@@ -78,6 +78,7 @@ struct EventEntity: AppEntity {
let dueDate: Date?
var isRecurring: Bool = false
var recurrenceLabel: String? = nil
var createdAt: Date? = nil
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Event"
static var defaultQuery = EventQuery()
@@ -105,7 +106,8 @@ struct EventQuery: EntityQuery {
.filter { !$0.isComplete && $0.dueDate != nil }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
.map { EventEntity(id: $0.id.uuidString, title: $0.title, dueDate: $0.dueDate,
isRecurring: $0.isRecurring, recurrenceLabel: $0.recurrenceLabel) }
isRecurring: $0.isRecurring, recurrenceLabel: $0.recurrenceLabel,
createdAt: $0.createdAt) }
}
}
@@ -235,7 +237,8 @@ struct EventProvider: AppIntentTimelineProvider {
.filter { !$0.isComplete && ($0.dueDate ?? .distantPast) > now }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
.map { EventEntity(id: $0.id.uuidString, title: $0.title, dueDate: $0.dueDate,
isRecurring: $0.isRecurring, recurrenceLabel: $0.recurrenceLabel) }
isRecurring: $0.isRecurring, recurrenceLabel: $0.recurrenceLabel,
createdAt: $0.createdAt) }
}
/// The tracked task ("Track Countdown"), but only while it's still live: not
@@ -267,13 +270,16 @@ struct EventProvider: AppIntentTimelineProvider {
/// Resolve the progress window (start target) for any event. Recurring events
/// (Mode B) span the current occurrence period: previous next occurrence
/// so a birthday 5 days away reads ~98% full. One-off events (Mode A) count
/// from an explicit start, the tracking date, or first-seen, up to the due date.
/// from an explicit start, the task's real creation date, the tracking date,
/// or first-seen, up to the due date in that priority order.
private func resolveSpan(due: Date, isRecurring: Bool, label: String?,
anchorKey: String, explicitStart: Date?) -> (start: Date, target: Date) {
anchorKey: String, explicitStart: Date?,
taskCreatedAt: Date? = nil) -> (start: Date, target: Date) {
if isRecurring, let label, let span = Self.recurrenceSpan(label: label, due: due) {
return (span.prev, span.next)
}
if let s = explicitStart, s < due { return (s, due) }
if let created = taskCreatedAt, created < due { return (created, due) }
return (Self.storedAnchor(key: anchorKey, target: due), due)
}
@@ -288,7 +294,8 @@ struct EventProvider: AppIntentTimelineProvider {
let trackedId = task.id.uuidString
let since = UserDefaults.kisani.object(forKey: "kisani.widget.trackedSince") as? Date
let s = resolveSpan(due: task.dueDate ?? now, isRecurring: task.isRecurring,
label: task.recurrenceLabel, anchorKey: trackedId, explicitStart: since)
label: task.recurrenceLabel, anchorKey: trackedId, explicitStart: since,
taskCreatedAt: task.createdAt)
return EventEntry(date: now, eventName: task.title, start: s.start, target: s.target, unitMode: mode)
}
@@ -302,12 +309,13 @@ struct EventProvider: AppIntentTimelineProvider {
}
let ev = upcoming[idx]
let s = resolveSpan(due: due, isRecurring: ev.isRecurring, label: ev.recurrenceLabel,
anchorKey: ev.id, explicitStart: nil)
anchorKey: ev.id, explicitStart: nil, taskCreatedAt: ev.createdAt)
return EventEntry(date: now, eventName: ev.title, start: s.start, target: s.target, unitMode: mode)
}
if let ev = config.event, let due = ev.dueDate {
let s = resolveSpan(due: due, isRecurring: ev.isRecurring, label: ev.recurrenceLabel,
anchorKey: ev.id, explicitStart: config.startDate)
anchorKey: ev.id, explicitStart: config.startDate,
taskCreatedAt: ev.createdAt)
return EventEntry(date: now, eventName: ev.title, start: s.start, target: s.target, unitMode: mode)
}
// Fully custom event (no task behind it).