diff --git a/TODO.md b/TODO.md index 4e987b1..34187e8 100644 --- a/TODO.md +++ b/TODO.md @@ -33,6 +33,10 @@ tested. # Completed Steps +- 2026-10-27: the Age column on the AS and prefix pages reads in words, such as + "5 minutes ago" or "2 weeks ago", instead of Go durations such as "360h0m0s + ago". An age under a minute reads in seconds, and one over a month in months + or years instead of a date (closes #163) - 2026-10-26: a write to a stored AS, peering or peer keeps the earlier of the two first-seen times and the later of the two last-seen times. An old update from a collector catching up on a backlog no longer moves last seen back diff --git a/internal/server/handlers_internal_test.go b/internal/server/handlers_internal_test.go index f4a87f3..0110958 100644 --- a/internal/server/handlers_internal_test.go +++ b/internal/server/handlers_internal_test.go @@ -512,6 +512,71 @@ func TestPagesSayOriginZeroIsNoKnownOrigin(t *testing.T) { } } +// TestPagesShowAgesInWords stores routes last updated about 5 minutes, 3 hours +// and 15 days ago, and checks that the AS page and the prefix pages show each +// age in words. Before the fix the ages read "5m0s ago", "3h0m0s ago" and +// "360h0m0s ago" (https://git.eeqj.de/sneak/routewatch/issues/163). +func TestPagesShowAgesInWords(t *testing.T) { + t.Parallel() + + db, err := database.New(&config.Config{StateDir: t.TempDir()}, logger.New()) + if err != nil { + t.Fatalf("database.New: %v", err) + } + + t.Cleanup(func() { _ = db.Close() }) + + // Each age is well past a whole number of its unit, so the time the test + // takes to run does not change the words. + ages := []struct { + prefix string + age time.Duration + want string + }{ + {testPrefixV4, 5*time.Minute + 30*time.Second, "5 minutes ago"}, + {"198.51.100.0/24", 3*time.Hour + 30*time.Minute, "3 hours ago"}, + {"203.0.113.0/24", 15 * 24 * time.Hour, "2 weeks ago"}, + } + + now := time.Now().UTC() + routes := make([]*database.LiveRoute, 0, len(ages)) + + for _, a := range ages { + routes = append(routes, &database.LiveRoute{ + Prefix: a.prefix, MaskLength: 24, IPVersion: 4, + OriginASN: 64500, PeerIP: testPeerV4, ASPath: []int{64500}, + NextHop: testPeerV4, LastUpdated: now.Add(-a.age), + }) + } + + err = db.UpsertLiveRouteBatch(routes) + if err != nil { + t.Fatalf("UpsertLiveRouteBatch: %v", err) + } + + s := New(db, streamer.New(logger.New(), metrics.New()), logger.New(), &config.Config{}) + + for _, a := range ages { + cell := `