diff --git a/cmd/mavweb/main.go b/cmd/mavweb/main.go index 17e49af..f0c8b42 100644 --- a/cmd/mavweb/main.go +++ b/cmd/mavweb/main.go @@ -18,6 +18,7 @@ import ( "net/url" "os" "os/signal" + "strconv" "strings" "time" @@ -865,17 +866,29 @@ func handleReminders(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) { } } +// now — the wall clock, indirected so the task page can be rendered at a fixed +// instant in a test. internal/tasks is pure and the daemon path already ranks +// through a clock it is handed; the page had no reason to be the one surface +// that could only be tested at whatever time it happened to run. +var now = time.Now + +// resolvedShown — how many finished tasks the page renders. The list is +// history, it only grows, and the rows below the first screen are read by +// nobody. +const resolvedShown = 50 + // taskRow is one line on /tasks, with every timestamp already formatted so the // template holds no date logic. type taskRow struct { - ID int64 - Text string - Source string - Evidence string - Status string - Due string - Created string - Resolved string + ID int64 + Text string + Source string + Evidence string + Status string + Due string + Created string + Resolved string + ResolvedBy string // Why — the ranker's reason for this row's position (Vikunja #129), in // Russian, empty when nothing distinguished the task. Blank is the honest // rendering: he never said this one mattered more. @@ -923,6 +936,7 @@ func handleTasks(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) { // rows keep store order (newest first) — ranking finished work is pointless. var live []tasks.Item var resolved []taskRow + resolvedTotal := 0 for _, t := range all { switch t.Status { case "candidate", "open": @@ -931,10 +945,18 @@ func handleTasks(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) { Created: t.CreatedTs, Due: t.Due, Weight: t.Weight, }) default: + resolvedTotal++ + // Finished work is history, and the history only grows. The page + // showed every row that ever existed, which is a page that gets + // slower every month for a section nobody reads past the top of. + if len(resolved) >= resolvedShown { + continue + } resolved = append(resolved, taskRow{ ID: t.ID, Text: t.Text, Source: t.Source, Evidence: t.Evidence, Status: t.Status, Created: fmtTaskTime(&t.CreatedTs), Due: fmtTaskDate(t.Due), Resolved: fmtTaskTime(t.Resolved), + ResolvedBy: t.ResolvedBy, }) } } @@ -943,7 +965,7 @@ func handleTasks(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) { byID[t.ID] = t } var cands, open []taskRow - for _, r := range tasks.Rank(live, time.Now()) { + for _, r := range tasks.Rank(live, now()) { t := byID[r.ID] row := taskRow{ ID: t.ID, Text: t.Text, Source: t.Source, Evidence: t.Evidence, @@ -962,11 +984,12 @@ func handleTasks(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) { } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := tasksTmpl.Execute(w, struct { - Msg, Err string - Candidates []taskRow - Open []taskRow - Resolved []taskRow - }{msg, errMsg, cands, open, resolved}); err != nil { + Msg, Err string + Candidates []taskRow + Open []taskRow + Resolved []taskRow + ResolvedMore bool + }{msg, errMsg, cands, open, resolved, resolvedTotal > len(resolved)}); err != nil { log.Printf("tasks render: %v", err) } } @@ -981,12 +1004,14 @@ func applyTaskPost(ctx context.Context, core ipc.CoreAPI, r *http.Request) (stri if text == "" { return "", errors.New("empty task text") } - req := ipc.CaptureTaskReq{Text: text, Source: "tap:web", Status: "open", Ts: time.Now()} + req := ipc.CaptureTaskReq{Text: text, Source: "tap:web", Status: "open", Ts: now()} // Importance is his, stated on the form. Out-of-range values are // clamped rather than rejected — a bad select is not worth a 400. if v := r.FormValue("weight"); v != "" { - var wgt int - if n, _ := fmt.Sscanf(v, "%d", &wgt); n != 1 || wgt < 0 { + // strconv, not Sscanf: Sscanf("3junk", "%d") succeeds with 3, and a + // form value is not a place to accept trailing garbage. + wgt, err := strconv.Atoi(v) + if err != nil || wgt < 0 { return "", fmt.Errorf("bad weight %q", v) } if wgt > tasks.MaxWeight { @@ -995,7 +1020,7 @@ func applyTaskPost(ctx context.Context, core ipc.CoreAPI, r *http.Request) (stri req.Weight = wgt } if d := r.FormValue("due"); d != "" { - due, err := time.ParseInLocation("2006-01-02", d, time.Local) + due, err := time.ParseInLocation("2006-01-02", d, now().Location()) if err != nil { return "", fmt.Errorf("bad due date %q", d) } @@ -1005,14 +1030,17 @@ func applyTaskPost(ctx context.Context, core ipc.CoreAPI, r *http.Request) (stri if err != nil { return "", err } + if resp.Promoted { + return "confirmed a candidate maven had found", nil + } if !resp.Created { return "already on the list", nil } return "added task", nil } - var id int64 - if n, _ := fmt.Sscanf(r.FormValue("id"), "%d", &id); n != 1 { + id, err := strconv.ParseInt(r.FormValue("id"), 10, 64) + if err != nil { return "", errors.New("invalid id") } var status, msg string @@ -1026,7 +1054,7 @@ func applyTaskPost(ctx context.Context, core ipc.CoreAPI, r *http.Request) (stri default: return "", fmt.Errorf("unknown action %q", action) } - if err := core.SetTaskStatus(ctx, id, status, time.Now()); err != nil { + if err := core.SetTaskStatus(ctx, id, status, now(), "tap:web"); err != nil { return "", err } return msg, nil diff --git a/cmd/mavweb/tasks.html b/cmd/mavweb/tasks.html index 8194506..70f6797 100644 --- a/cmd/mavweb/tasks.html +++ b/cmd/mavweb/tasks.html @@ -9,6 +9,9 @@ +