issue 1858: answer the operator's list for a mode (1858.3)
ADR 0517 §4, ADR 0515 §1: list gives tasks under today, this week and later by due date in the operator's zone, promises by kind, good to know, read later, handled today with its undo and the scam warnings, for the mode's accounts, and when the operator last looked.
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package main
|
||||
|
||||
// The operator's list (novox/hq ADR 0515 §1, ADR 0517 §1, §2, §4; issue 1858): a pure function over the
|
||||
// entries, the handlings and the day, answered by list. Days are the operator's (days.go, Europe/Brussels).
|
||||
//
|
||||
// - A task falls under today when it is due today or before, this week when due by the end of Sunday, and
|
||||
// later when due after that or not at all.
|
||||
// - A promise falls under its kind: you promised, waiting on others, on its way.
|
||||
// - A reminded item is hidden until the day it shows again, and that day it is under today; the mail
|
||||
// itself reminded hides all its items, and shows them, or itself when it has none, under today that day.
|
||||
// - A handled item is gone from its section; handled today, it is under handled today with its undo. The
|
||||
// mail itself handled takes all its items with it.
|
||||
// - Good to know holds the entries with no task or promise, not read later, not a scam warning and not
|
||||
// handled; read later the read-later ones; a scam warning is in its own place and nowhere else.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultListLimit bounds each section when list is not asked another limit.
|
||||
const DefaultListLimit = 50
|
||||
|
||||
// ListItem is one line of the list: an item of an entry, with what the page shows of its mail.
|
||||
type ListItem struct {
|
||||
Entry Entry
|
||||
Item string // ItemMail, "task n" or "promise n"
|
||||
Kind string // "task", "promise" or "mail"
|
||||
Line string
|
||||
Due time.Time // a task's due date or a promise's date; zero for none
|
||||
Promise PromiseKind
|
||||
Who string
|
||||
Reminded bool // shown under today because it was reminded
|
||||
Handled time.Time // for handled today
|
||||
}
|
||||
|
||||
// OperatorList is the list for one mode.
|
||||
type OperatorList struct {
|
||||
Today, ThisWeek, Later []ListItem
|
||||
YouPromised, WaitingOnOthers, OnItsWay []ListItem
|
||||
GoodToKnow, ReadLater, ScamWarnings []ListItem // mail items
|
||||
HandledToday []ListItem
|
||||
}
|
||||
|
||||
// BuildList builds the list of the entries of accounts (every account when accounts is empty) for the day of
|
||||
// now.
|
||||
func BuildList(entries []Entry, handlings []Handling, accounts []string, now time.Time) OperatorList {
|
||||
today := dayOf(now)
|
||||
sunday := endOfWeek(today)
|
||||
byItem := map[int64]map[string]Handling{}
|
||||
for _, h := range handlings {
|
||||
if byItem[h.Entry] == nil {
|
||||
byItem[h.Entry] = map[string]Handling{}
|
||||
}
|
||||
byItem[h.Entry][h.Item] = h
|
||||
}
|
||||
wanted := map[string]bool{}
|
||||
for _, a := range accounts {
|
||||
wanted[a] = true
|
||||
}
|
||||
var l OperatorList
|
||||
for _, e := range entries {
|
||||
if len(wanted) > 0 && !wanted[e.Account] {
|
||||
continue
|
||||
}
|
||||
hs := byItem[e.ID]
|
||||
mail := hs[ItemMail]
|
||||
mailItem := ListItem{Entry: e, Item: ItemMail, Kind: "mail", Line: e.Summary}
|
||||
if !mail.Handled.IsZero() {
|
||||
if dayOf(mail.Handled).Equal(today) {
|
||||
mailItem.Handled = mail.Handled
|
||||
l.HandledToday = append(l.HandledToday, mailItem)
|
||||
}
|
||||
continue // the mail itself handled takes all its items with it
|
||||
}
|
||||
if !mail.ShowsAgain.IsZero() && mail.ShowsAgain.After(today) {
|
||||
continue // the mail itself reminded: hidden, all of it, until its day
|
||||
}
|
||||
mailToday := mail.ShowsAgain.Equal(today)
|
||||
if e.Mark == MarkScamWarning {
|
||||
l.ScamWarnings = append(l.ScamWarnings, mailItem)
|
||||
continue
|
||||
}
|
||||
items := itemsOf(e)
|
||||
for _, it := range items {
|
||||
h := hs[it.Item]
|
||||
switch {
|
||||
case !h.Handled.IsZero():
|
||||
if dayOf(h.Handled).Equal(today) {
|
||||
it.Handled = h.Handled
|
||||
l.HandledToday = append(l.HandledToday, it)
|
||||
}
|
||||
case !h.ShowsAgain.IsZero() && h.ShowsAgain.After(today):
|
||||
// reminded: hidden until its day
|
||||
case h.ShowsAgain.Equal(today) || mailToday:
|
||||
it.Reminded = true
|
||||
l.Today = append(l.Today, it)
|
||||
case it.Kind == "task":
|
||||
switch {
|
||||
case !it.Due.IsZero() && !it.Due.After(today):
|
||||
l.Today = append(l.Today, it)
|
||||
case !it.Due.IsZero() && !it.Due.After(sunday):
|
||||
l.ThisWeek = append(l.ThisWeek, it)
|
||||
default:
|
||||
l.Later = append(l.Later, it)
|
||||
}
|
||||
default:
|
||||
switch it.Promise {
|
||||
case YouPromised:
|
||||
l.YouPromised = append(l.YouPromised, it)
|
||||
case WaitingOnOthers:
|
||||
l.WaitingOnOthers = append(l.WaitingOnOthers, it)
|
||||
default:
|
||||
l.OnItsWay = append(l.OnItsWay, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case len(items) == 0 && mailToday:
|
||||
mailItem.Reminded = true
|
||||
l.Today = append(l.Today, mailItem)
|
||||
case e.ReadLater:
|
||||
l.ReadLater = append(l.ReadLater, mailItem)
|
||||
case len(items) == 0:
|
||||
l.GoodToKnow = append(l.GoodToKnow, mailItem)
|
||||
}
|
||||
}
|
||||
for _, section := range [][]ListItem{l.Today, l.ThisWeek, l.Later, l.YouPromised, l.WaitingOnOthers, l.OnItsWay} {
|
||||
sortByDue(section)
|
||||
}
|
||||
for _, section := range [][]ListItem{l.GoodToKnow, l.ReadLater, l.ScamWarnings} {
|
||||
sort.SliceStable(section, func(i, j int) bool { return section[i].Entry.Date.After(section[j].Entry.Date) })
|
||||
}
|
||||
sort.SliceStable(l.HandledToday, func(i, j int) bool { return l.HandledToday[i].Handled.After(l.HandledToday[j].Handled) })
|
||||
return l
|
||||
}
|
||||
|
||||
// itemsOf is an entry's tasks and promises as items, in their numbers' order.
|
||||
func itemsOf(e Entry) []ListItem {
|
||||
var out []ListItem
|
||||
for _, t := range e.Tasks {
|
||||
out = append(out, ListItem{Entry: e, Item: "task " + strconv.Itoa(t.N), Kind: "task", Line: t.Line, Due: t.Due})
|
||||
}
|
||||
for _, p := range e.Promises {
|
||||
out = append(out, ListItem{Entry: e, Item: "promise " + strconv.Itoa(p.N), Kind: "promise", Line: p.Line, Due: p.Date,
|
||||
Promise: p.Kind, Who: p.Who})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// sortByDue orders a section nearest date first, none last, then newest mail first.
|
||||
func sortByDue(section []ListItem) {
|
||||
sort.SliceStable(section, func(i, j int) bool {
|
||||
a, b := section[i], section[j]
|
||||
if a.Due.IsZero() != b.Due.IsZero() {
|
||||
return b.Due.IsZero()
|
||||
}
|
||||
if !a.Due.Equal(b.Due) {
|
||||
return a.Due.Before(b.Due)
|
||||
}
|
||||
return a.Entry.Date.After(b.Entry.Date)
|
||||
})
|
||||
}
|
||||
|
||||
// listItemOut is one line as list answers it: never a mail body, only the mail's headers, its summary and the
|
||||
// model's own lines.
|
||||
func listItemOut(it ListItem) map[string]any {
|
||||
e := it.Entry
|
||||
out := map[string]any{
|
||||
"id": e.ID, "item": it.Item, "kind": it.Kind, "line": it.Line, "account": e.Account,
|
||||
"sender-name": e.SenderName, "sender-address": e.SenderAddress, "subject": e.Subject,
|
||||
"date": e.Date.UTC().Format(time.RFC3339), "summary": e.Summary, "mark": string(e.Mark), "why": e.Why,
|
||||
}
|
||||
if !it.Due.IsZero() {
|
||||
key := "due-date"
|
||||
if it.Kind == "promise" {
|
||||
key = "promise-date"
|
||||
}
|
||||
out[key] = it.Due.Format("2006-01-02")
|
||||
}
|
||||
if it.Kind == "promise" {
|
||||
out["promise-kind"] = string(it.Promise)
|
||||
out["who"] = it.Who
|
||||
}
|
||||
if it.Reminded {
|
||||
out["reminded"] = true
|
||||
}
|
||||
if !it.Handled.IsZero() {
|
||||
out["handled"] = it.Handled.UTC().Format(time.RFC3339)
|
||||
out["undo"] = true // until the day ends (ADR 0517 §1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func sectionOut(items []ListItem, limit int) []map[string]any {
|
||||
if len(items) > limit {
|
||||
items = items[:limit]
|
||||
}
|
||||
out := make([]map[string]any, len(items))
|
||||
for i, it := range items {
|
||||
out[i] = listItemOut(it)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package main
|
||||
|
||||
// Issue 1858 (novox/hq ADR 0517 §1, §2, §4; ADR 0515 §1, §4): the operator's list for a mode — tasks under
|
||||
// today, this week and later by their due date (Europe/Brussels), promises by kind, good to know, read later,
|
||||
// handled today with its undo, the scam warnings in their own place — and when the operator last looked.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// listOf calls list and answers it as the page reads it: JSON.
|
||||
type listed struct {
|
||||
Mode string `json:"mode"`
|
||||
TodayIs string `json:"today-is"`
|
||||
LastLooked string `json:"last-looked"`
|
||||
Today []map[string]any `json:"today"`
|
||||
ThisWeek []map[string]any `json:"this-week"`
|
||||
Later []map[string]any `json:"later"`
|
||||
YouPromised []map[string]any `json:"you-promised"`
|
||||
WaitingOnOthers []map[string]any `json:"waiting-on-others"`
|
||||
OnItsWay []map[string]any `json:"on-its-way"`
|
||||
GoodToKnow []map[string]any `json:"good-to-know"`
|
||||
ReadLater []map[string]any `json:"read-later"`
|
||||
HandledToday []map[string]any `json:"handled-today"`
|
||||
ScamWarnings []map[string]any `json:"scam-warnings"`
|
||||
}
|
||||
|
||||
func listOf(t *testing.T, b *Bundle, args map[string]any) listed {
|
||||
t.Helper()
|
||||
out, err := callTool(t, b, ListTool, args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(out)
|
||||
var l listed
|
||||
if err := json.Unmarshal(raw, &l); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// lineSet is the lines of a section, each as "<id> <item>".
|
||||
func lineSet(section []map[string]any) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, it := range section {
|
||||
out[jsonID(it["id"])+" "+it["item"].(string)] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func jsonID(v any) string { raw, _ := json.Marshal(v); return string(raw) }
|
||||
|
||||
func idItem(id int64, item string) string { return jsonID(id) + " " + item }
|
||||
|
||||
func addEntry(t *testing.T, store Store, e Entry) int64 {
|
||||
t.Helper()
|
||||
if e.Date.IsZero() {
|
||||
e.Date = time.Date(2026, 10, 20, 8, 0, 0, 0, time.UTC)
|
||||
}
|
||||
if e.Mark == "" {
|
||||
e.Mark = MarkNeither
|
||||
}
|
||||
id, err := store.AddEntry(context.Background(), e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func day(y int, m time.Month, d int) time.Time { return time.Date(y, m, d, 0, 0, 0, 0, time.UTC) }
|
||||
|
||||
// 1858/AC1: handle task 2, and list shows task 1 and no longer task 2; handle the mail itself, and none of
|
||||
// its items shows.
|
||||
func TestHandlingATaskOrTheMailHidesItsItems(t *testing.T) {
|
||||
store := newMemStore()
|
||||
id := entryWithItems(t, store)
|
||||
now := brusselsAt(2026, 10, 22, 9, 0)
|
||||
b := listBundle(store, &now)
|
||||
if _, err := callTool(t, b, HandleTool, map[string]any{"id": float64(id), "item": "task 2"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l := listOf(t, b, map[string]any{})
|
||||
open := lineSet(append(append(l.Today, l.ThisWeek...), l.Later...))
|
||||
if !open[idItem(id, "task 1")] || open[idItem(id, "task 2")] {
|
||||
t.Fatalf("tasks listed %v, want task 1 and not task 2", open)
|
||||
}
|
||||
if _, err := callTool(t, b, HandleTool, map[string]any{"id": float64(id)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l = listOf(t, b, map[string]any{})
|
||||
for name, section := range map[string][]map[string]any{"today": l.Today, "this week": l.ThisWeek, "later": l.Later,
|
||||
"on its way": l.OnItsWay, "good to know": l.GoodToKnow, "read later": l.ReadLater} {
|
||||
if len(section) != 0 {
|
||||
t.Fatalf("%s holds %v after the mail itself was handled, want nothing of it", name, section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1858/AC2: an item handled today is under handled today, with its undo; unhandle puts it back in its
|
||||
// section; after local midnight it is no longer under handled today.
|
||||
func TestHandledTodayHasItsUndoUntilTheDayEnds(t *testing.T) {
|
||||
store := newMemStore()
|
||||
id := entryWithItems(t, store)
|
||||
now := brusselsAt(2026, 10, 22, 22, 30) // 20:30 UTC: still the 22nd in Brussels
|
||||
b := listBundle(store, &now)
|
||||
if _, err := callTool(t, b, HandleTool, map[string]any{"id": float64(id), "item": "task 1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l := listOf(t, b, map[string]any{})
|
||||
if !lineSet(l.HandledToday)[idItem(id, "task 1")] || l.HandledToday[0]["undo"] != true {
|
||||
t.Fatalf("handled today = %v, want task 1 with its undo", l.HandledToday)
|
||||
}
|
||||
if lineSet(l.Later)[idItem(id, "task 1")] {
|
||||
t.Fatal("a handled task is still in its section")
|
||||
}
|
||||
if _, err := callTool(t, b, UnhandleTool, map[string]any{"id": float64(id), "item": "task 1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l = listOf(t, b, map[string]any{})
|
||||
if len(l.HandledToday) != 0 || !lineSet(l.Later)[idItem(id, "task 1")] {
|
||||
t.Fatalf("after unhandle: handled today %v, later %v; want task 1 back under later", l.HandledToday, l.Later)
|
||||
}
|
||||
if _, err := callTool(t, b, HandleTool, map[string]any{"id": float64(id), "item": "task 1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now = brusselsAt(2026, 10, 23, 0, 1) // 22:01 UTC on the 22nd: the next day in Brussels
|
||||
l = listOf(t, b, map[string]any{})
|
||||
if len(l.HandledToday) != 0 || lineSet(l.Later)[idItem(id, "task 1")] {
|
||||
t.Fatalf("after midnight: handled today %v, later %v; want task 1 in neither", l.HandledToday, l.Later)
|
||||
}
|
||||
}
|
||||
|
||||
// 1858/AC3: a reminded item is hidden the same day, and under today the next.
|
||||
func TestARemindedItemShowsUnderTodayTheNextDay(t *testing.T) {
|
||||
store := newMemStore()
|
||||
id := entryWithItems(t, store)
|
||||
now := brusselsAt(2026, 10, 22, 9, 0)
|
||||
b := listBundle(store, &now)
|
||||
if _, err := callTool(t, b, RemindTool, map[string]any{"id": float64(id), "item": "promise 1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l := listOf(t, b, map[string]any{})
|
||||
if lineSet(l.OnItsWay)[idItem(id, "promise 1")] || lineSet(l.Today)[idItem(id, "promise 1")] {
|
||||
t.Fatalf("the reminded promise shows the same day: on its way %v, today %v", l.OnItsWay, l.Today)
|
||||
}
|
||||
now = brusselsAt(2026, 10, 23, 7, 0)
|
||||
l = listOf(t, b, map[string]any{})
|
||||
if !lineSet(l.Today)[idItem(id, "promise 1")] || lineSet(l.OnItsWay)[idItem(id, "promise 1")] {
|
||||
t.Fatalf("the next day: today %v, on its way %v; want the promise under today", l.Today, l.OnItsWay)
|
||||
}
|
||||
}
|
||||
|
||||
// 1858/AC5: due today, Saturday of this week, next month and none fall under today, this week, later and
|
||||
// later; promises by kind; good to know holds the entries with no task or promise, not read later, not a
|
||||
// scam warning and not handled; read later holds the read-later ones; the mode's accounts alone; and the
|
||||
// answer carries when the operator last looked.
|
||||
func TestTheListsSections(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newMemStore()
|
||||
now := brusselsAt(2026, 10, 22, 9, 0) // a Thursday
|
||||
b := listBundle(store, &now)
|
||||
tasks := addEntry(t, store, Entry{Account: "work", UID: 1, Subject: "four tasks", Tasks: []Task{
|
||||
{N: 1, Line: "due today", Due: day(2026, 10, 22)},
|
||||
{N: 2, Line: "due Saturday", Due: day(2026, 10, 24)},
|
||||
{N: 3, Line: "due next month", Due: day(2026, 11, 20)},
|
||||
}})
|
||||
noDue := addEntry(t, store, Entry{Account: "work", UID: 2, Tasks: []Task{{N: 1, Line: "no due date"}}})
|
||||
overdue := addEntry(t, store, Entry{Account: "work", UID: 3, Tasks: []Task{{N: 1, Line: "overdue", Due: day(2026, 10, 20)}}})
|
||||
promises := addEntry(t, store, Entry{Account: "work", UID: 4, Promises: []Promise{
|
||||
{N: 1, Kind: YouPromised, Line: "send the slides"}, {N: 2, Kind: WaitingOnOthers, Line: "Tim's quote"},
|
||||
{N: 3, Kind: OnItsWay, Line: "the Telco router"},
|
||||
}})
|
||||
know := addEntry(t, store, Entry{Account: "work", UID: 5, Subject: "a newsletter"})
|
||||
later := addEntry(t, store, Entry{Account: "work", UID: 6, ReadLater: true})
|
||||
scam := addEntry(t, store, Entry{Account: "work", UID: 7, Mark: MarkScamWarning, Tasks: []Task{{N: 1, Line: "pay the fake invoice"}}})
|
||||
handled := addEntry(t, store, Entry{Account: "work", UID: 8, Subject: "handled long ago"})
|
||||
_ = store.Act(ctx, Handling{Entry: handled, Item: ItemMail, Handled: now.AddDate(0, 0, -3)}, ActionLine{What: ActHandled})
|
||||
personal := addEntry(t, store, Entry{Account: "personal", UID: 9, Subject: "Hostco bill", Tasks: []Task{{N: 1, Line: "pay Hostco"}}})
|
||||
looked := now.Add(-2 * time.Hour)
|
||||
_ = store.Record(ctx, ActionLine{What: ActPageOpened, Mode: "work", At: looked})
|
||||
|
||||
l := listOf(t, b, map[string]any{"mode": "work", "accounts": []any{"work"}})
|
||||
want := map[string]map[string]bool{
|
||||
"today": {idItem(tasks, "task 1"): true, idItem(overdue, "task 1"): true},
|
||||
"this week": {idItem(tasks, "task 2"): true},
|
||||
"later": {idItem(tasks, "task 3"): true, idItem(noDue, "task 1"): true},
|
||||
"you promised": {idItem(promises, "promise 1"): true},
|
||||
"waiting on others": {idItem(promises, "promise 2"): true},
|
||||
"on its way": {idItem(promises, "promise 3"): true},
|
||||
"good to know": {idItem(know, "mail"): true},
|
||||
"read later": {idItem(later, "mail"): true},
|
||||
"scam warnings": {idItem(scam, "mail"): true},
|
||||
}
|
||||
got := map[string]map[string]bool{
|
||||
"today": lineSet(l.Today), "this week": lineSet(l.ThisWeek), "later": lineSet(l.Later),
|
||||
"you promised": lineSet(l.YouPromised), "waiting on others": lineSet(l.WaitingOnOthers), "on its way": lineSet(l.OnItsWay),
|
||||
"good to know": lineSet(l.GoodToKnow), "read later": lineSet(l.ReadLater), "scam warnings": lineSet(l.ScamWarnings),
|
||||
}
|
||||
for name, w := range want {
|
||||
if len(got[name]) != len(w) {
|
||||
t.Errorf("%s = %v, want %v", name, got[name], w)
|
||||
continue
|
||||
}
|
||||
for k := range w {
|
||||
if !got[name][k] {
|
||||
t.Errorf("%s = %v, want %v", name, got[name], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
if l.Mode != "work" || l.TodayIs != "2026-10-22" || l.LastLooked != looked.UTC().Format(time.RFC3339) {
|
||||
t.Errorf("mode %q, today-is %q, last-looked %q; want work, 2026-10-22, %s", l.Mode, l.TodayIs, l.LastLooked, looked.UTC().Format(time.RFC3339))
|
||||
}
|
||||
if t.Failed() {
|
||||
return
|
||||
}
|
||||
// Everything: every account, the personal one's task too.
|
||||
all := listOf(t, b, map[string]any{"mode": "everything"})
|
||||
if !lineSet(all.Later)[idItem(personal, "task 1")] {
|
||||
t.Fatalf("everything's later = %v, want the personal account's task too", all.Later)
|
||||
}
|
||||
// A task line carries what the page shows: its line, its due date, its mail's sender and subject.
|
||||
for _, it := range l.ThisWeek {
|
||||
if it["line"] != "due Saturday" || it["due-date"] != "2026-10-24" || it["kind"] != "task" || it["subject"] != "four tasks" {
|
||||
t.Fatalf("this week's task = %v", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,3 +183,71 @@ func (b *Bundle) remindTool(args map[string]any) (any, error) {
|
||||
h.ShowsAgain = dayOf(now).AddDate(0, 0, 1)
|
||||
return a.act(RemindTool, ActReminded, h, now)
|
||||
}
|
||||
|
||||
// listTool is the operator's list for a mode (ADR 0517 §4): the mode's accounts (mesh-mail-web names them from
|
||||
// its setting modes; every account when none is given), each section at most limit lines, and when the
|
||||
// operator last looked (the newest page opened line). Read-only: the page opened is record's to write.
|
||||
func (b *Bundle) listTool(args map[string]any) (any, error) {
|
||||
if _, err := b.checkCaller(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := only(args, "mode", "accounts", "limit"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mode, err := modeArg(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var accounts []string
|
||||
if v, has := args["accounts"]; has && v != nil {
|
||||
list, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s needs accounts to be a list of account names", ListTool)
|
||||
}
|
||||
for _, a := range list {
|
||||
s, ok := a.(string)
|
||||
if !ok || s == "" {
|
||||
return nil, fmt.Errorf("%s needs accounts to be a list of account names", ListTool)
|
||||
}
|
||||
accounts = append(accounts, s)
|
||||
}
|
||||
}
|
||||
limit, err := number(args, "limit", DefaultListLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = DefaultListLimit
|
||||
}
|
||||
ctx := context.Background()
|
||||
store, err := b.Store(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := store.Entries(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
handlings, err := store.Handlings(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := b.now()
|
||||
l := BuildList(entries, handlings, accounts, now)
|
||||
out := map[string]any{
|
||||
"mode": mode, "today-is": dayOf(now).Format("2006-01-02"),
|
||||
"today": sectionOut(l.Today, limit), "this-week": sectionOut(l.ThisWeek, limit), "later": sectionOut(l.Later, limit),
|
||||
"you-promised": sectionOut(l.YouPromised, limit), "waiting-on-others": sectionOut(l.WaitingOnOthers, limit),
|
||||
"on-its-way": sectionOut(l.OnItsWay, limit), "good-to-know": sectionOut(l.GoodToKnow, limit),
|
||||
"read-later": sectionOut(l.ReadLater, limit), "scam-warnings": sectionOut(l.ScamWarnings, limit),
|
||||
"handled-today": sectionOut(l.HandledToday, limit),
|
||||
}
|
||||
looked, ok, err := store.LastAction(ctx, ActPageOpened)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
out["last-looked"] = looked.UTC().Format(time.RFC3339)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestTheManifestClaimsMailReadingAndInvokesReadOnly(t *testing.T) {
|
||||
t.Fatalf("runs-as %q, want %q", m.RunsAs, Module)
|
||||
}
|
||||
wantClaims := []seat{
|
||||
{Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status", "handle", "unhandle", "remind"}},
|
||||
{Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status", "list", "handle", "unhandle", "remind"}},
|
||||
}
|
||||
if !reflect.DeepEqual(m.Claims, wantClaims) {
|
||||
t.Fatalf("claims %+v, want %+v", m.Claims, wantClaims)
|
||||
|
||||
@@ -196,6 +196,19 @@ func (b *Bundle) Tools() []stdio.Tool {
|
||||
"no mail read says so). Answered only to a caller the setting callers names. Read-only.",
|
||||
Input: schema(nil, map[string]any{}),
|
||||
Run: b.statusTool},
|
||||
{Name: ListTool,
|
||||
Description: "The operator's list for a mode (novox/hq ADR 0517 §4, ADR 0515 §1): tasks under today, this " +
|
||||
"week and later by their due date (Europe/Brussels), promises by kind (you promised, waiting on " +
|
||||
"others, on its way), good to know, read later, handled today with its undo and the scam warnings, " +
|
||||
"each line an item (a task, a promise or the mail itself) with its mail's headers and summary — " +
|
||||
"never a mail body — and when the operator last looked. Answered only to a caller the setting " +
|
||||
"callers names. Read-only.",
|
||||
Input: schema(nil, map[string]any{
|
||||
"mode": text("work, personal or everything (when left out)"),
|
||||
"accounts": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the mode's accounts, by name; every account when left out"},
|
||||
"limit": integer("how many lines each section answers at most; 50 when left out"),
|
||||
}),
|
||||
Run: b.listTool},
|
||||
{Name: HandleTool,
|
||||
Description: "Mark one item of a mail entry handled for the operator (novox/hq ADR 0517 §1): a task or a " +
|
||||
"promise by its number, or the mail itself, which handles all its items with it. It changes nothing " +
|
||||
|
||||
@@ -21,9 +21,7 @@ func TestToolsListsTheSixVerbsAndHealth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
want := []string{
|
||||
"mail-reading.entries", "mail-reading.groups", "mail-reading.correct", "mail-reading.lessons",
|
||||
"mail-reading.undo", "mail-reading.status", "mail-reading.handle", "mail-reading.unhandle", "mail-reading.remind",
|
||||
"mail_reader_health",
|
||||
"mail-reading.entries", "mail-reading.groups", "mail-reading.correct", "mail-reading.lessons", "mail-reading.undo", "mail-reading.status", "mail-reading.list", "mail-reading.handle", "mail-reading.unhandle", "mail-reading.remind", "mail_reader_health",
|
||||
}
|
||||
if len(names) != len(want) {
|
||||
t.Fatalf("Tools() = %v, want %v", names, want)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "mail-reading",
|
||||
"scope": "mesh",
|
||||
"serves": ["entries", "groups", "correct", "lessons", "undo", "status", "handle", "unhandle", "remind"]
|
||||
"serves": ["entries", "groups", "correct", "lessons", "undo", "status", "list", "handle", "unhandle", "remind"]
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
@@ -48,6 +48,7 @@
|
||||
"mail-reading.lessons",
|
||||
"mail-reading.undo",
|
||||
"mail-reading.status",
|
||||
"mail-reading.list",
|
||||
"mail-reading.handle",
|
||||
"mail-reading.unhandle",
|
||||
"mail-reading.remind",
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
{Name: "mail-store", Scope: "mesh", Serves: []string{"accounts", "search", "read", "send", "send-status"}},
|
||||
{Name: "mail-webclient", Scope: "mesh", Serves: []string{"open", "status"}},
|
||||
{Name: "mail-notifier", Scope: "node", Serves: []string{"status"}, Needs: []string{"node-notifier"}},
|
||||
{Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status", "handle", "unhandle", "remind"}},
|
||||
{Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status", "list", "handle", "unhandle", "remind"}},
|
||||
}
|
||||
if !reflect.DeepEqual(m.Seats, wantSeats) {
|
||||
t.Fatalf("seats %+v, want %+v", m.Seats, wantSeats)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{
|
||||
"name": "mail-reading",
|
||||
"scope": "mesh",
|
||||
"serves": ["entries", "groups", "correct", "lessons", "undo", "status", "handle", "unhandle", "remind"]
|
||||
"serves": ["entries", "groups", "correct", "lessons", "undo", "status", "list", "handle", "unhandle", "remind"]
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
||||
Reference in New Issue
Block a user