Files
joakim 80ea17227d fix: prevent nil-panic on server and improve OAuth callback handling
Load config eagerly during server startup so sortByUrgency never
hits a nil config. Add nil-guard in BuildUrgencyCoefficients as
belt-and-suspenders defense. Fix OAuth callback to support both
GET and POST, and resolve issuer URLs properly with path.Dir.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:40:53 +01:00

195 lines
4.8 KiB
Go

package engine
// UrgencyCoefficients holds the configurable coefficients for urgency calculation
type UrgencyCoefficients struct {
Due float64
PriorityH float64
PriorityM float64
PriorityD float64
PriorityL float64
Active float64
Age float64
AgeMax int
Tags float64
Project float64
Waiting float64
UrgentTag string // Configurable urgent tag name (default: "next")
UrgentCoeff float64 // Coefficient for urgent tag
}
// CalculateUrgency computes the urgency score for a task
func (t *Task) CalculateUrgency(coeffs *UrgencyCoefficients) float64 {
urgency := 0.0
// Due date contribution
urgency += t.urgencyDue(coeffs.Due)
// Priority contribution
urgency += t.urgencyPriority(coeffs)
// Age contribution
urgency += t.urgencyAge(coeffs.Age, coeffs.AgeMax)
// Active task contribution
urgency += t.urgencyActive(coeffs.Active)
// Waiting task penalty
urgency += t.urgencyWaiting(coeffs.Waiting)
// Tags contribution
urgency += t.urgencyTags(coeffs)
// Project contribution
urgency += t.urgencyProject(coeffs.Project)
return urgency
}
// urgencyDue calculates urgency contribution from due date
// Linear scale: overdue=12.0, today=10.0, 7 days=6.0, 14+ days=2.0
func (t *Task) urgencyDue(coeff float64) float64 {
if t.Due == nil {
return 0.0
}
now := timeNow()
due := *t.Due
// Calculate days until due (negative if overdue)
duration := due.Sub(now)
daysUntil := duration.Hours() / 24.0
var scale float64
if daysUntil < 0 {
// Overdue - maximum urgency
scale = 12.0
} else if daysUntil < 1 {
// Due today
scale = 10.0
} else if daysUntil <= 7 {
// Due within a week - linear from 10.0 to 6.0
scale = 10.0 - (daysUntil * 0.571) // (10-6)/7 = 0.571
} else if daysUntil <= 14 {
// Due within two weeks - linear from 6.0 to 2.0
scale = 6.0 - ((daysUntil - 7.0) * 0.571) // (6-2)/7 = 0.571
} else {
// Due further out - low urgency
scale = 2.0
}
return scale * coeff
}
// urgencyPriority calculates urgency contribution from priority
// Order: High > Medium > Default > Low
func (t *Task) urgencyPriority(coeffs *UrgencyCoefficients) float64 {
switch t.Priority {
case PriorityHigh:
return coeffs.PriorityH
case PriorityMedium:
return coeffs.PriorityM
case PriorityDefault:
return coeffs.PriorityD
case PriorityLow:
return coeffs.PriorityL
default:
return 0.0
}
}
// urgencyAge calculates urgency contribution from task age
// Age increases linearly up to maxDays, then caps
func (t *Task) urgencyAge(coeff float64, maxDays int) float64 {
now := timeNow()
ageDuration := now.Sub(t.Created)
ageDays := int(ageDuration.Hours() / 24.0)
if ageDays > maxDays {
ageDays = maxDays
}
ageRatio := float64(ageDays) / float64(maxDays)
return ageRatio * coeff
}
// urgencyActive calculates urgency contribution for started tasks
func (t *Task) urgencyActive(coeff float64) float64 {
if t.Start != nil {
return coeff
}
return 0.0
}
// urgencyWaiting calculates urgency penalty for waiting tasks
func (t *Task) urgencyWaiting(coeff float64) float64 {
if t.Wait != nil {
now := timeNow()
if t.Wait.After(now) {
return coeff // Should be negative coefficient
}
}
return 0.0
}
// urgencyTags calculates urgency contribution from tags
// Special urgent tag (configurable, default "next") has high coefficient
// Regular tags contribute with diminishing returns (0.8 for 1, 0.9 for 2, 1.0 for 3+)
func (t *Task) urgencyTags(coeffs *UrgencyCoefficients) float64 {
tagCount := len(t.Tags)
if tagCount == 0 {
return 0.0
}
// Check for special urgent tag first
for _, tag := range t.Tags {
if tag == coeffs.UrgentTag {
return coeffs.UrgentCoeff
}
}
// Apply general tag coefficient with multiplier based on count
var multiplier float64
switch tagCount {
case 1:
multiplier = 0.8
case 2:
multiplier = 0.9
default:
multiplier = 1.0
}
return multiplier * coeffs.Tags
}
// urgencyProject calculates urgency contribution from having a project
func (t *Task) urgencyProject(coeff float64) float64 {
if t.Project != nil {
return coeff
}
return 0.0
}
// BuildUrgencyCoefficients creates UrgencyCoefficients from config.
// If cfg is nil, uses DefaultConfig() to prevent nil-pointer panics.
func BuildUrgencyCoefficients(cfg *Config) *UrgencyCoefficients {
if cfg == nil {
cfg = DefaultConfig()
}
return &UrgencyCoefficients{
Due: cfg.UrgencyDue,
PriorityH: cfg.UrgencyPriorityH,
PriorityM: cfg.UrgencyPriorityM,
PriorityD: cfg.UrgencyPriorityD,
PriorityL: cfg.UrgencyPriorityL,
Active: cfg.UrgencyActive,
Age: cfg.UrgencyAge,
AgeMax: cfg.UrgencyAgeMax,
Tags: cfg.UrgencyTags,
Project: cfg.UrgencyProject,
Waiting: cfg.UrgencyWaiting,
UrgentTag: cfg.UrgencyUrgentTag,
UrgentCoeff: cfg.UrgencyUrgentCoeff,
}
}