tasks: count overdue by calendar day like the ranker does (V-581)

Stalls compared the due instant to now while Rank compares whole calendar days, so a task due at 09:00 was counted overdue from 09:01 while its own row on the same page still read the reason as today. Stalls now reads dayDelta.

A row with no capture time also counted as sitting, because the zero time is January of year 1 and every span from it clears ten days. Rank already guarded that and Stalls did not.

Folded the open-versus-candidate partition FormatRU and Spoken each carried into one split helper. The two have to agree on where that line falls.
This commit is contained in:
2026-08-06 03:12:28 +04:00
parent 316fb197a8
commit 85456d3833
3 changed files with 45 additions and 18 deletions
+16 -16
View File
@@ -208,14 +208,7 @@ const SpokenLimit = 5
// DayPlan.Spoken is built core-side: two formatters drift, and then she says
// one order and shows another.
func FormatRU(ranked []Ranked) string {
var open, cands []Ranked
for _, r := range ranked {
if r.Status == StatusCandidate {
cands = append(cands, r)
} else {
open = append(open, r)
}
}
open, cands := split(ranked)
if len(open) == 0 && len(cands) == 0 {
return say.S(say.TasksNone, nil)
}
@@ -243,6 +236,20 @@ func FormatRU(ranked []Ranked) string {
return b.String()
}
// split separates confirmed work from candidates, preserving Rank's order
// within each half. FormatRU and Spoken have to agree on where the line falls,
// so they read it from one place.
func split(ranked []Ranked) (open, cands []Ranked) {
for _, r := range ranked {
if r.Status == StatusCandidate {
cands = append(cands, r)
} else {
open = append(open, r)
}
}
return open, cands
}
// joinRU lists up to limit tasks, then says how many are left. withReasons
// attaches the parenthesised reason — candidates are listed bare, since their
// due dates are Maven's reading of a mail and not something he stated.
@@ -273,14 +280,7 @@ func joinRU(rs []Ranked, limit int, withReasons bool) string {
// an ordinal resolves against is built here and not by a caller guessing how
// the renderer split and truncated it.
func Spoken(ranked []Ranked) []Ranked {
var open, cands []Ranked
for _, r := range ranked {
if r.Status == StatusCandidate {
cands = append(cands, r)
} else {
open = append(open, r)
}
}
open, cands := split(ranked)
out := make([]Ranked, 0, 2*SpokenLimit)
for _, group := range [][]Ranked{open, cands} {
if len(group) > SpokenLimit {