3bb2ef2759
Fix latent API bug where multi-word fields (RecurrenceDuration, ParentUUID, CreatedAt) serialized as PascalCase, breaking the frontend. Add explicit snake_case json tags and custom MarshalJSON/UnmarshalJSON on Task, Status, and APIKey to emit unix timestamps and string status codes. Add Urgency float64 as a derived field on Task, populated via PopulateUrgency helper in all handlers before serialization. The report engine's sortByUrgency now also retains the computed score. Frontend updated with urgency type, color-coded badge in TaskItem, and mock data values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
447 lines
11 KiB
Go
447 lines
11 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
)
|
|
|
|
// DisplayFormat defines how tasks should be displayed
|
|
type DisplayFormat string
|
|
|
|
const (
|
|
DisplayFormatTable DisplayFormat = "table"
|
|
DisplayFormatMinimal DisplayFormat = "minimal"
|
|
)
|
|
|
|
// Report defines a named task report with filters and display options
|
|
type Report struct {
|
|
Name string
|
|
Description string
|
|
BaseFilter *Filter // Base filter that cannot be overridden
|
|
DisplayFormat DisplayFormat // How to display results
|
|
SortFunc func([]*Task) []*Task
|
|
LimitFunc func([]*Task) []*Task
|
|
}
|
|
|
|
// AllReports returns all predefined reports
|
|
func AllReports() map[string]*Report {
|
|
return map[string]*Report{
|
|
"active": ActiveReport(),
|
|
"all": AllReport(),
|
|
"completed": CompletedReport(),
|
|
"list": ListReport(),
|
|
"minimal": MinimalReport(),
|
|
"newest": NewestReport(),
|
|
"next": NextReport(),
|
|
"oldest": OldestReport(),
|
|
"overdue": OverdueReport(),
|
|
"ready": ReadyReport(),
|
|
"recurring": RecurringReport(),
|
|
"template": TemplateReport(),
|
|
"waiting": WaitingReport(),
|
|
}
|
|
}
|
|
|
|
// GetReport retrieves a report by name
|
|
func GetReport(name string) (*Report, error) {
|
|
reports := AllReports()
|
|
report, exists := reports[name]
|
|
if !exists {
|
|
return nil, fmt.Errorf("unknown report: %s", name)
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
// ActiveReport shows started tasks
|
|
func ActiveReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
filter.Attributes["_started"] = "true" // Special marker for tasks with start time
|
|
|
|
return &Report{
|
|
Name: "active",
|
|
Description: "Started tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
SortFunc: sortByUrgency,
|
|
}
|
|
}
|
|
|
|
// AllReport shows all tasks (no status filter)
|
|
func AllReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["_all"] = "true" // Special marker to include templates
|
|
|
|
return &Report{
|
|
Name: "all",
|
|
Description: "All tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
}
|
|
}
|
|
|
|
// CompletedReport shows completed tasks
|
|
func CompletedReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "completed"
|
|
|
|
return &Report{
|
|
Name: "completed",
|
|
Description: "Completed tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
}
|
|
}
|
|
|
|
// ListReport shows pending tasks (default report)
|
|
func ListReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
|
|
return &Report{
|
|
Name: "list",
|
|
Description: "Pending tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
SortFunc: sortByUrgency,
|
|
}
|
|
}
|
|
|
|
// MinimalReport shows pending tasks in minimal format
|
|
func MinimalReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
|
|
return &Report{
|
|
Name: "minimal",
|
|
Description: "Pending tasks (minimal format)",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatMinimal,
|
|
SortFunc: sortByUrgency,
|
|
}
|
|
}
|
|
|
|
// NewestReport shows most recent pending tasks
|
|
func NewestReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
|
|
return &Report{
|
|
Name: "newest",
|
|
Description: "Most recent pending tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
SortFunc: func(tasks []*Task) []*Task {
|
|
// Sort by created descending
|
|
sorted := make([]*Task, len(tasks))
|
|
copy(sorted, tasks)
|
|
for i := 0; i < len(sorted)-1; i++ {
|
|
for j := i + 1; j < len(sorted); j++ {
|
|
if sorted[i].Created.Before(sorted[j].Created) {
|
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
|
}
|
|
}
|
|
}
|
|
return sorted
|
|
},
|
|
LimitFunc: func(tasks []*Task) []*Task {
|
|
if len(tasks) > 10 {
|
|
return tasks[:10]
|
|
}
|
|
return tasks
|
|
},
|
|
}
|
|
}
|
|
|
|
// NextReport shows most urgent tasks ready to work on
|
|
func NextReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
filter.Attributes["_ready"] = "true"
|
|
|
|
return &Report{
|
|
Name: "next",
|
|
Description: "Most urgent tasks ready to work on",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
SortFunc: func(tasks []*Task) []*Task {
|
|
// Sort by urgency descending
|
|
cfg, _ := GetConfig()
|
|
coeffs := BuildUrgencyCoefficients(cfg)
|
|
|
|
sorted := make([]*Task, len(tasks))
|
|
copy(sorted, tasks)
|
|
|
|
for i := 0; i < len(sorted)-1; i++ {
|
|
for j := i + 1; j < len(sorted); j++ {
|
|
urgI := sorted[i].CalculateUrgency(coeffs)
|
|
urgJ := sorted[j].CalculateUrgency(coeffs)
|
|
if urgI < urgJ {
|
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return sorted
|
|
},
|
|
LimitFunc: func(tasks []*Task) []*Task {
|
|
cfg, _ := GetConfig()
|
|
limit := cfg.NextLimit
|
|
if limit <= 0 {
|
|
limit = 5
|
|
}
|
|
if len(tasks) > limit {
|
|
return tasks[:limit]
|
|
}
|
|
return tasks
|
|
},
|
|
}
|
|
}
|
|
|
|
// OldestReport shows oldest pending tasks
|
|
func OldestReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
|
|
return &Report{
|
|
Name: "oldest",
|
|
Description: "Oldest pending tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
SortFunc: func(tasks []*Task) []*Task {
|
|
// Sort by created ascending (already default, but explicit)
|
|
sorted := make([]*Task, len(tasks))
|
|
copy(sorted, tasks)
|
|
for i := 0; i < len(sorted)-1; i++ {
|
|
for j := i + 1; j < len(sorted); j++ {
|
|
if sorted[i].Created.After(sorted[j].Created) {
|
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
|
}
|
|
}
|
|
}
|
|
return sorted
|
|
},
|
|
}
|
|
}
|
|
|
|
// OverdueReport shows overdue tasks
|
|
func OverdueReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
filter.Attributes["_overdue"] = "true" // Special marker for overdue tasks
|
|
|
|
return &Report{
|
|
Name: "overdue",
|
|
Description: "Overdue tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
SortFunc: sortByUrgency,
|
|
}
|
|
}
|
|
|
|
// ReadyReport shows tasks ready to work on
|
|
func ReadyReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
filter.Attributes["_ready"] = "true" // Special marker for ready tasks
|
|
|
|
return &Report{
|
|
Name: "ready",
|
|
Description: "Tasks ready to work on",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
SortFunc: sortByUrgency,
|
|
}
|
|
}
|
|
|
|
// RecurringReport shows pending recurring instances
|
|
func RecurringReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
filter.Attributes["_recurring_instance"] = "true" // Special marker for recurring instances
|
|
|
|
return &Report{
|
|
Name: "recurring",
|
|
Description: "Pending recurring task instances",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
}
|
|
}
|
|
|
|
// TemplateReport shows recurring template tasks
|
|
func TemplateReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "template" // Special status for templates
|
|
|
|
return &Report{
|
|
Name: "template",
|
|
Description: "Recurring template tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
}
|
|
}
|
|
|
|
// WaitingReport shows waiting/hidden tasks
|
|
func WaitingReport() *Report {
|
|
filter := NewFilter()
|
|
filter.Attributes["status"] = "pending"
|
|
filter.Attributes["_waiting"] = "true" // Special marker for waiting tasks
|
|
|
|
return &Report{
|
|
Name: "waiting",
|
|
Description: "Hidden/waiting tasks",
|
|
BaseFilter: filter,
|
|
DisplayFormat: DisplayFormatTable,
|
|
}
|
|
}
|
|
|
|
// Execute runs the report with optional additional filters
|
|
func (r *Report) Execute(additionalFilters []string) ([]*Task, error) {
|
|
// Start with base filter
|
|
filter := r.BaseFilter
|
|
|
|
// Merge additional filters if provided
|
|
if len(additionalFilters) > 0 {
|
|
userFilter, err := ParseFilter(additionalFilters)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse additional filters: %w", err)
|
|
}
|
|
|
|
// Merge filters (user filters add to base, but cannot override status)
|
|
filter = mergeFilters(r.BaseFilter, userFilter)
|
|
}
|
|
|
|
// Get tasks
|
|
tasks, err := GetTasks(filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply post-filter for special markers
|
|
tasks = r.applyPostFilters(tasks)
|
|
|
|
// Apply sorting if defined
|
|
if r.SortFunc != nil {
|
|
tasks = r.SortFunc(tasks)
|
|
}
|
|
|
|
// Apply limit if defined
|
|
if r.LimitFunc != nil {
|
|
tasks = r.LimitFunc(tasks)
|
|
}
|
|
|
|
return tasks, nil
|
|
}
|
|
|
|
// applyPostFilters applies special filters that can't be done in SQL
|
|
func (r *Report) applyPostFilters(tasks []*Task) []*Task {
|
|
now := timeNow()
|
|
filtered := []*Task{}
|
|
|
|
for _, task := range tasks {
|
|
include := true
|
|
|
|
// Check for _started marker
|
|
if r.BaseFilter.Attributes["_started"] == "true" {
|
|
if task.Start == nil {
|
|
include = false
|
|
}
|
|
}
|
|
|
|
// Check for _overdue marker
|
|
if r.BaseFilter.Attributes["_overdue"] == "true" {
|
|
if task.Due == nil || !task.Due.Before(now) {
|
|
include = false
|
|
}
|
|
}
|
|
|
|
// Check for _ready marker
|
|
if r.BaseFilter.Attributes["_ready"] == "true" {
|
|
// Task is ready if scheduled and wait are either null or in the past
|
|
if task.Scheduled != nil && task.Scheduled.After(now) {
|
|
include = false
|
|
}
|
|
if task.Wait != nil && task.Wait.After(now) {
|
|
include = false
|
|
}
|
|
}
|
|
|
|
// Check for _waiting marker
|
|
if r.BaseFilter.Attributes["_waiting"] == "true" {
|
|
if task.Wait == nil || !task.Wait.After(now) {
|
|
include = false
|
|
}
|
|
}
|
|
|
|
// Check for _recurring_instance marker
|
|
if r.BaseFilter.Attributes["_recurring_instance"] == "true" {
|
|
if task.ParentUUID == nil {
|
|
include = false
|
|
}
|
|
}
|
|
|
|
if include {
|
|
filtered = append(filtered, task)
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
// mergeFilters combines base filter with user filter
|
|
func mergeFilters(base, user *Filter) *Filter {
|
|
merged := NewFilter()
|
|
|
|
// Copy base attributes (these take precedence)
|
|
for k, v := range base.Attributes {
|
|
merged.Attributes[k] = v
|
|
}
|
|
|
|
// Add user attributes (but don't override status or special markers)
|
|
for k, v := range user.Attributes {
|
|
if k != "status" && k[0] != '_' {
|
|
merged.Attributes[k] = v
|
|
}
|
|
}
|
|
|
|
// Merge tags
|
|
merged.IncludeTags = append(merged.IncludeTags, base.IncludeTags...)
|
|
merged.IncludeTags = append(merged.IncludeTags, user.IncludeTags...)
|
|
|
|
merged.ExcludeTags = append(merged.ExcludeTags, base.ExcludeTags...)
|
|
merged.ExcludeTags = append(merged.ExcludeTags, user.ExcludeTags...)
|
|
|
|
// Merge IDs and UUIDs
|
|
merged.IDs = append(merged.IDs, base.IDs...)
|
|
merged.IDs = append(merged.IDs, user.IDs...)
|
|
|
|
merged.UUIDs = append(merged.UUIDs, base.UUIDs...)
|
|
merged.UUIDs = append(merged.UUIDs, user.UUIDs...)
|
|
|
|
return merged
|
|
}
|
|
|
|
// sortByUrgency is a helper function to sort tasks by urgency (descending).
|
|
// It also populates the Urgency field on each task so the score is available in responses.
|
|
func sortByUrgency(tasks []*Task) []*Task {
|
|
cfg, _ := GetConfig()
|
|
coeffs := BuildUrgencyCoefficients(cfg)
|
|
|
|
sorted := make([]*Task, len(tasks))
|
|
copy(sorted, tasks)
|
|
|
|
// Calculate and store urgency on each task
|
|
for _, t := range sorted {
|
|
t.Urgency = t.CalculateUrgency(coeffs)
|
|
}
|
|
|
|
for i := 0; i < len(sorted)-1; i++ {
|
|
for j := i + 1; j < len(sorted); j++ {
|
|
if sorted[i].Urgency < sorted[j].Urgency {
|
|
sorted[i], sorted[j] = sorted[j], sorted[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return sorted
|
|
}
|