Overdue badges on the task board
Go · Go · beginner · modification
Adds an `overdue` flag to every task in the project task list, so the board can put a red badge on tasks that are past their due date. The rule lives in a small `isOverdue` function with its own table test: a task is overdue when it is not done and was due before now, so a task due exactly now, a finished task and a task with no due date stay unflagged. The list handler applies it to each task before encoding the response, reading the time from a clock injected into `Handler` (`time.Now` in `main`). The flag is computed per response, nothing new is stored.
Go 1.22 service, standard library only (`net/http` method-pattern `ServeMux`, `log/slog`, `database/sql`). `auth.RequireMember` (existing, unchanged) answers 401 or 403 itself unless the signed-in user is a member of the project in the path. `store.NewTaskStore(db).ListByProject` (existing, unchanged) reads the project's tasks in board order, at most 500 per project; it returns a non-nil, possibly empty slice (an empty project encodes as `[]`), and each `DueAt` is either nil or a `timestamptz` value. `config.Load`, `store.Open` and `httpserver.Serve` are unchanged; `httpserver.Serve(ctx, addr, h)` sets the server timeouts and drains in-flight requests on shutdown. The board's badge (web app) ships separately and reads `overdue` from this endpoint; until this PR, the field is absent and the board shows no badges.
Requirements
- `GET /api/projects/{id}/tasks` (existing) returns the project's tasks as a JSON array. Each task gains a boolean `overdue` field so the board can show a red badge: it is `true` when the task is not done and its due time is before the current time, and `false` otherwise. A task due exactly at the current time is not overdue yet, and a task without a due date (`due_at` is `null`) is never overdue.
- Overdue is decided on instants: `due_at` is a point in time, and the viewer's time zone plays no part (the board formats the date in the viewer's zone itself). The current time comes from an injected clock (`Handler.Now`); production passes `time.Now`.
- `overdue` is computed for each response and never stored: the store neither reads nor writes it, and `Task` is used only as this endpoint's response shape.
- Unchanged: a path id that is not a positive integer answers 400; the store's error is logged and answers 500 `internal error`; the tasks keep the order the store returns them in, and every other field of a task is returned as before.
Files touched
- cmd/api/main.go
- internal/tasks/tasks.go
- internal/tasks/handler.go
- internal/tasks/overdue.go
- internal/tasks/overdue_test.go
--- cmd/api/main.go
"os/signal"
"syscall"
+ "time"
"example.com/taskboard/internal/auth"
defer db.Close()
- th := &tasks.Handler{Store: store.NewTaskStore(db), Log: log}
+ th := &tasks.Handler{Store: store.NewTaskStore(db), Log: log, Now: time.Now}
mux := http.NewServeMux()
--- internal/tasks/tasks.go
// Task is one row of a project's task list, as the board renders it.
type Task struct {
- ID int64 `json:"id"`
- Title string `json:"title"`
- Done bool `json:"done"`
- DueAt *time.Time `json:"due_at"`
+ ID int64 `json:"id"`
+ Title string `json:"title"`
+ Done bool `json:"done"`
+ DueAt *time.Time `json:"due_at"`
+ Overdue bool `json:"overdue"`
}
Store Store
Log *slog.Logger
+ Now func() time.Time
}