diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index 0142e77a..0a38bd0a 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -121,7 +121,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase public bool HasBranch => !string.IsNullOrWhiteSpace(Branch); public bool HasDiff => DiffAdditions > 0 || DiffDeletions > 0; public bool HasSteps => StepsCount > 0; - public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done; + public bool IsOverdue => ScheduledFor is { } d && d.ToLocalTime().Date < DateTime.Today && !Done; public bool IsRunning => Status == TaskStatus.Running; public bool IsWaitingForReview => Status == TaskStatus.WaitingForReview; // Parked = set aside from review: Idle but still holding its Active worktree (vs a plain Idle diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 73756c2e..1923ee08 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -637,7 +637,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable if (r.Done && !underOpenPlanningParent) completed.Add(r); - else if (r.ScheduledFor is { } d && d.Date < today) + else if (r.ScheduledFor is { } d && d.ToLocalTime().Date < today) overdue.Add(r); else open.Add(r); @@ -1361,12 +1361,18 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable public async Task SetScheduledForAsync(TaskRowViewModel row, DateTime? when) { if (row is null) return; + // ThemedDatePicker builds Unspecified/Local wall-clock values; the DB's UtcConverter only + // tags DateTimes as Utc on read, it never converts on write, so this is the one place that + // must turn "local wall clock" into a real UTC instant before it's persisted. + var whenUtc = when is { } w + ? (w.Kind == DateTimeKind.Utc ? w : DateTime.SpecifyKind(w, DateTimeKind.Local).ToUniversalTime()) + : (DateTime?)null; await using var db = await _dbFactory.CreateDbContextAsync(); var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id); if (entity is null) return; - entity.ScheduledFor = when; + entity.ScheduledFor = whenUtc; await db.SaveChangesAsync(); - row.ScheduledFor = when; + row.ScheduledFor = whenUtc; Regroup(); UpdateSubtitle(); TasksChanged?.Invoke(this, EventArgs.Empty); diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs index 93a56e8d..09bc4909 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs @@ -217,7 +217,7 @@ public partial class TaskRowView : UserControl { if (DataContext is not TaskRowViewModel row) return; _pendingScheduleRow = row; - ScheduleDate.SelectedDate = row.ScheduledFor ?? DateTime.Now.AddHours(1); + ScheduleDate.SelectedDate = row.ScheduledFor?.ToLocalTime() ?? DateTime.Now.AddHours(1); ScheduleAnchor.Flyout?.ShowAt(ScheduleAnchor); } diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandRegroupTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandRegroupTests.cs index 422c61b9..47fc4d04 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandRegroupTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandRegroupTests.cs @@ -346,4 +346,24 @@ public class TasksIslandRegroupTests : IDisposable Assert.Equal(new[] { "t2", "t3", "t1" }, vm.Items.Select(r => r.Id)); Assert.Equal(new[] { "t2", "t3", "t1" }, vm.Rows.OfType().Select(r => r.Id)); } + + [Fact] + public async Task SetScheduledForAsync_LocalDateTime_PersistsAsUtc() + { + await SeedTasksAsync(("t1", TaskStatus.Idle, null, 0)); + + var vm = BuildViewModel(); + await LoadAndWaitAsync(vm, UserList("list1", "Default")); + var row = vm.Items.First(r => r.Id == "t1"); + + // ThemedDatePicker hands SetScheduledForAsync Local/Unspecified wall-clock values. + var localWhen = DateTime.SpecifyKind(new DateTime(2026, 8, 26, 15, 0, 0), DateTimeKind.Local); + + await vm.SetScheduledForAsync(row, localWhen); + + await using var db = NewContext(); + var entity = await db.Tasks.FirstAsync(t => t.Id == "t1"); + Assert.Equal(DateTimeKind.Utc, entity.ScheduledFor!.Value.Kind); + Assert.Equal(localWhen.ToUniversalTime(), entity.ScheduledFor!.Value); + } }