Fire accepted routines every interval, not once (Vikunja #366)

The tick loop now reads accepted routines from the store and nudges when
their interval has passed; accepting no longer builds a one-shot reminder.
Look at routine.DueAccepted for the schedule rule (no catch-up backlog) and
at fireAcceptedRoutines for the restraint gate — routines do not bypass it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
kami
2026-07-31 02:45:30 +04:00
parent 54dc43516b
commit 424d1b3446
5 changed files with 249 additions and 19 deletions
+41
View File
@@ -55,6 +55,47 @@ func Validate(routines []Routine) error {
return nil
}
// Accepted — an accepted routine proposal as the tick driver sees it. This is a
// different shape from Routine: the schedule is a plain interval the pattern
// detector measured, not an operator-written cron expression. Accepted is when
// the human said yes; LastFired is nil until the first nudge.
type Accepted struct {
ID int64
Name string
IntervalDays float64
Accepted time.Time
LastFired *time.Time
}
// DueAccepted returns the accepted routines whose interval has passed. It does
// not mutate anything — the caller persists the new last-fired time, because
// that has to survive a restart (unlike Due's in-memory map).
//
// The clock starts at LastFired, or at Accepted for a routine that has never
// nudged. A routine with a non-positive interval never fires: a bad interval
// should mean silence, not a nudge every tick.
//
// One occurrence per call, no catch-up: the caller stamps the fire time as now,
// so a routine that was silent for a month nudges once and then waits a full
// interval. Never a backlog.
func DueAccepted(rs []Accepted, now time.Time) []Accepted {
var out []Accepted
for _, r := range rs {
if r.IntervalDays <= 0 {
continue
}
since := r.Accepted
if r.LastFired != nil {
since = *r.LastFired
}
gap := time.Duration(r.IntervalDays * 24 * float64(time.Hour))
if !now.Before(since.Add(gap)) {
out = append(out, r)
}
}
return out
}
// Due returns the routines whose schedule crossed since their last fire and
// records now as the new last-fire time for each one returned. The caller owns
// `last` (the tick driver holds it across ticks); Due mutates it in place.