Skip to content

Commit 058d22d

Browse files
committed
Skip no day when Basecamp's date is ahead of this machine's
The feed's undated request reads today in the person's Basecamp time zone, and the dated walk began at yesterday on this machine. When Basecamp's date was a day ahead, the machine's today was never read. The walk now begins at the machine's today; when the dates agree that day comes back twice, which the seen-set already absorbs, at one extra request for a quiet account. `timeline` without --date had the same gap in its breadcrumb, which named the machine's yesterday. Undated, the CLI can't know Basecamp's today, so the breadcrumb now offers `--date <YYYY-MM-DD>`; with --date it still names the exact day before.
1 parent a8e7078 commit 058d22d

4 files changed

Lines changed: 49 additions & 17 deletions

File tree

‎internal/commands/timeline.go‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -150,21 +150,23 @@ func runTimeline(cmd *cobra.Command, args []string, project, person, date string
150150
return convertSDKError(err)
151151
}
152152

153+
// Undated, the day is today in the person's Basecamp time zone, which
154+
// this machine's clock can't name, so neither can the day before it.
153155
summary := fmt.Sprintf("%d events today", len(result.Events))
154-
previous := basecamp.DateOf(time.Now().AddDate(0, 0, -1))
156+
earlier := output.Breadcrumb{Action: "earlier", Cmd: "basecamp timeline --date <YYYY-MM-DD>", Description: "View an earlier day"}
155157
if !day.IsZero() {
156158
summary = fmt.Sprintf("%d events on %s", len(result.Events), day)
157-
previous = basecamp.DateOf(day.In(time.Local).AddDate(0, 0, -1))
159+
earlier = output.Breadcrumb{
160+
Action: "previous",
161+
Cmd: fmt.Sprintf("basecamp timeline --date %s", basecamp.DateOf(day.In(time.Local).AddDate(0, 0, -1))),
162+
Description: "View the day before",
163+
}
158164
}
159165

160166
respOpts := []output.ResponseOption{
161167
output.WithSummary(summary),
162168
output.WithBreadcrumbs(
163-
output.Breadcrumb{
164-
Action: "previous",
165-
Cmd: fmt.Sprintf("basecamp timeline --date %s", previous),
166-
Description: "View the day before",
167-
},
169+
earlier,
168170
output.Breadcrumb{
169171
Action: "project",
170172
Cmd: "basecamp timeline --in <project>",

‎internal/commands/timeline_test.go‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package commands
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"errors"
57
"io"
68
"net/http"
@@ -28,6 +30,18 @@ func (p *progressTransport) RoundTrip(req *http.Request) (*http.Response, error)
2830
return &http.Response{StatusCode: http.StatusOK, Header: header, Body: io.NopCloser(strings.NewReader(`[]`)), Request: req}, nil
2931
}
3032

33+
func firstBreadcrumb(t *testing.T, buf *bytes.Buffer) string {
34+
t.Helper()
35+
var envelope struct {
36+
Breadcrumbs []struct {
37+
Cmd string `json:"cmd"`
38+
} `json:"breadcrumbs"`
39+
}
40+
require.NoError(t, json.Unmarshal(buf.Bytes(), &envelope))
41+
require.NotEmpty(t, envelope.Breadcrumbs)
42+
return envelope.Breadcrumbs[0].Cmd
43+
}
44+
3145
func TestTimelineDateReadsThatDay(t *testing.T) {
3246
transport := &progressTransport{}
3347
app, buf := newTestAppWithTransport(t, transport)
@@ -37,17 +51,21 @@ func TestTimelineDateReadsThatDay(t *testing.T) {
3751

3852
require.Len(t, transport.queries, 1)
3953
assert.Equal(t, "2026-08-30", transport.queries[0].Get("date"))
40-
assert.Contains(t, buf.String(), "basecamp timeline --date 2026-08-29")
54+
assert.Equal(t, "basecamp timeline --date 2026-08-29", firstBreadcrumb(t, buf))
4155
}
4256

4357
func TestTimelineWithoutDateReadsToday(t *testing.T) {
4458
transport := &progressTransport{}
45-
app, _ := newTestAppWithTransport(t, transport)
59+
app, buf := newTestAppWithTransport(t, transport)
60+
app.Flags.Hints = true
4661

4762
require.NoError(t, executeCommand(NewTimelineCmd(), app))
4863

4964
require.Len(t, transport.queries, 1)
5065
assert.False(t, transport.queries[0].Has("date"))
66+
// Undated, the day is Basecamp's today, which this machine's clock can't
67+
// name, so the breadcrumb can't name the day before it either.
68+
assert.Equal(t, "basecamp timeline --date <YYYY-MM-DD>", firstBreadcrumb(t, buf))
5169
}
5270

5371
func TestTimelineDateRefusals(t *testing.T) {

‎internal/tui/workspace/data/hub_global.go‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -608,22 +608,24 @@ func (h *Hub) Timeline() *Pool[[]TimelineEventInfo] {
608608
// timelineFeedSize is how many events the activity feed shows.
609609
const timelineFeedSize = 80
610610

611-
// timelineLookbackDays is how many days a quiet account's feed reads back.
611+
// timelineLookbackDays is how many days, today included, a quiet account's
612+
// feed reads.
612613
const timelineLookbackDays = 7
613614

614615
// fetchTimelineEvents fetches timeline events from a single account.
615616
// Basecamp serves the activity feed one day per request, so this asks for
616-
// today (undated, so in the person's Basecamp time zone) and then steps back
617-
// a day at a time from yesterday on this machine until the feed is full.
618-
// When the two time zones disagree on the date a day can come back twice,
619-
// so an event already seen is skipped.
617+
// today undated, which is today in the person's Basecamp time zone, then
618+
// steps back a day at a time from today on this machine until the feed is
619+
// full. Starting the dated walk at this machine's today, not yesterday, keeps
620+
// a day from being skipped when Basecamp's date is ahead; when the dates
621+
// agree that day comes back twice, so an event already seen is skipped.
620622
func fetchTimelineEvents(ctx context.Context, client *basecamp.AccountClient, acct AccountInfo) ([]TimelineEventInfo, error) {
621623
var events []basecamp.TimelineEvent
622624
seen := make(map[int64]bool)
623625
today := hubNow()
624-
for back := 0; back < timelineLookbackDays && len(events) < timelineFeedSize; back++ {
626+
for back := -1; back < timelineLookbackDays && len(events) < timelineFeedSize; back++ {
625627
opts := &basecamp.ProgressOptions{Limit: timelineFeedSize - len(events)}
626-
if back > 0 {
628+
if back >= 0 {
627629
opts.Date = basecamp.DateOf(today.AddDate(0, 0, -back))
628630
}
629631
result, err := client.Timeline().Progress(ctx, opts)

‎internal/tui/workspace/data/timeline_feed_test.go‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func TestFetchTimelineEventsStopsAtTheLookback(t *testing.T) {
8989
}}
9090

9191
assert.Empty(t, fetchActivityDays(t, transport))
92-
assert.Equal(t, []string{"", "2026-10-22", "2026-10-21", "2026-10-20", "2026-10-19", "2026-10-18", "2026-10-17"}, transport.asked)
92+
assert.Equal(t, []string{"", "2026-10-23", "2026-10-22", "2026-10-21", "2026-10-20", "2026-10-19", "2026-10-18", "2026-10-17"}, transport.asked)
9393
}
9494

9595
func TestFetchTimelineEventsKeepsAnEventSeenTwiceOnce(t *testing.T) {
@@ -100,3 +100,13 @@ func TestFetchTimelineEventsKeepsAnEventSeenTwiceOnce(t *testing.T) {
100100

101101
assert.Equal(t, []int64{1, 2, 3}, fetchActivityDays(t, transport))
102102
}
103+
104+
func TestFetchTimelineEventsMissesNoDayWhenBasecampIsAhead(t *testing.T) {
105+
transport := &activityDaysTransport{today: "2026-10-24", days: map[string][]int64{
106+
"2026-10-24": {1},
107+
"2026-10-23": {2},
108+
"2026-10-22": {3},
109+
}}
110+
111+
assert.Equal(t, []int64{1, 2, 3}, fetchActivityDays(t, transport))
112+
}

0 commit comments

Comments
 (0)