From 86e1f20e11e298b2f18f1cdb3876d8c0880bc3b8 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 27 Oct 2026 01:23:50 +0100 Subject: [PATCH] Show ages on the AS and prefix pages in words (closes #163) The Age column printed Go's duration format for every plural age, such as "5m0s ago" or "360h0m0s ago". The templates now register humanize.Time from go-humanize, which the server already uses, as the timeSince function, and the hand-written function is gone. Ages read "5 minutes ago", "3 hours ago" or "2 weeks ago". An age under a minute reads "now" or in seconds instead of "just now". An age over a month reads in months or years instead of a date. Model: opus-5-5 --- TODO.md | 4 ++ internal/server/handlers_internal_test.go | 65 +++++++++++++++++++++++ internal/templates/templates.go | 47 ++-------------- 3 files changed, 72 insertions(+), 44 deletions(-) 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 := `