Skip to content

feat(webapp): a "What's new" page anchored at your last visit (BEA-65) - #109

Open
ssowonny wants to merge 2 commits into
mainfrom
bea-65-ph-idea-what-changed-while-i-was-away
Open

feat(webapp): a "What's new" page anchored at your last visit (BEA-65)#109
ssowonny wants to merge 2 commits into
mainfrom
bea-65-ph-idea-what-changed-while-i-was-away

Conversation

@ssowonny

@ssowonny ssowonny commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

TL;DR

  • New What's new page per project (/<project-id>/since): everything that changed since you were last here, agent runs still folded into one card each.
  • The anchor is a localStorage last-visit marker plus a five-line ?since= filter on the history API. That's the whole feature — the page below the header is HistoryView unchanged.
  • The marker is frozen at mount and stamped after entries render, and only this page writes it — so it can't empty itself while you read it, and no other view marks your changes as seen.
  • First visit (fresh browser, private mode, storage off) falls back to the last 7 days and says so.
  • Known gap: the marker is per browser, not per account — laptop and phone keep separate last-visit times. The server-side per-account marker is the larger follow-up named in the issue's CTO gate.

Closes BEA-65.

What it looks like

Before — History, flat reverse-chron After — What's new, anchored
before after

Revisit, once the marker has advanced:

empty

Server

?since=<RFC3339> on GET /api/p/<id>/history — one case in the filter switch that already handles path/prefix, and a parse next to ?n= (unparseable → 400 invalid since, same style as invalid n / invalid cursor). Absent, the response is byte-for-byte what it was.

Placement is the whole correctness story, and it is unchanged from the plan:

loadOps → journal.Sort → kinds[] classification   ← still walks ALL ops
                              ↓
                    filter: path / prefix / SINCE  ← new case here
                              ↓
                    sort.Slice(histLess) → cursor skip → n → next_cursor

Filtering after kinds[] keeps add-vs-edit labelling identical to the full feed; filtering before the sort means next_cursor is minted from the filtered list, so paging a since-feed terminates on its own oldest match instead of walking the whole project.

TestHistorySince covers: only-newer, strictly-after (an op at exactly the marker is excluded), composition with prefix= and path=, a very old marker being identical to no marker at all, cursor paging to exhaustion, and the 400.

Client

  • since joins VIEW_ROUTES / ViewName — a real route, so deep link, reload, back/forward and SPA fallback all come free.
  • lib/lastVisit.ts — the first localStorage in the frontend. Keyed bdrive.lastVisit.<account>.<project> (the one deviation from the spec, called out in the plan: two people on one laptop shouldn't share a last-visit time). Every access is try/caught, so private mode degrades to "always the last 7 days" rather than a white screen.
  • SinceView — ~80 lines: reads the baseline from a useState initializer (frozen for the mount), renders the header line, and stamps behind a useRef guard so "Load more" can't restamp. It is the only caller of stampVisit.
  • HistoryView gains three optional props (since, emptyText, onLoaded). since is part of qs, which is also the queryKey, so a revisit with a fresh baseline refetches instead of reusing the previous visit's page. With all three absent /history renders exactly as before.
  • Nav row "What's new" (clock icon) above History; crumb, active state, styles.

Not done, per the issue's Out column: no unread badge, no subtree scoping, no "mark all as read", no server-side marker.

Architecture changes

architecture/webapp-frontend.md: router's VIEW_ROUTES gains since; components gains SinceView; lib gains lastVisit.ts — the one storage-touching module in a folder documented as pure, so its note says so. No class was removed and no relationship changed. webapp-server.md names no history types and overview.md no new package, so neither changes.

✅ added · ❌ removed (strikethrough) · unmarked = unchanged

flowchart TB
    Browser["<div style='text-align:left'><b>Browser</b><br/>folder listing, file view<br/>per-view routes</div>"]
    router["<div style='text-align:left'><b>router</b><br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +VIEW_ROUTES dashboard history <b>since</b> install settings</span><br/>+LEGACY_VIEWS insights to dashboard<br/>+parseRoute(url, mode) Route<br/>+urlForView / encodePath / decodePath</div>"]
    components["<div style='text-align:left'><b>components</b><br/>FileView FolderListing FileTree<br/>HistoryView HistoryRow DiffView<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ SinceView</span><br/>Insights ShareDialog Palette shell ...</div>"]
    lib["<div style='text-align:left'><b>lib</b><br/>+diff.ts splitLines lcsDiff diffText<br/>+runs.ts groupRuns runFileCount<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +lastVisit.ts lastVisit stampVisit (localStorage)</span><br/>+heat.ts / sniff.ts / utils.ts</div>"]
    Marker["lastVisit.ts is the one storage-touching<br/>module in an otherwise pure lib/ —<br/>the only localStorage in the frontend.<br/>SinceView is its only writer, so no<br/>other page can move the marker."]
    Browser --> router
    Browser --> components
    components --> lib
    lib -.- Marker
    classDef added fill:#22c55e22,stroke:#22c55e,stroke-width:2px
    classDef noteBox fill:#88888822,stroke:#888888,stroke-dasharray:2 2
    class Marker noteBox
Loading

Verification

  • go build ./..., go vet ./..., go test ./... — all pass, including the new TestHistorySince.
  • npm run e2e109 passed, including the new e2e/since.spec.ts (first-visit 7-day fallback, a change landing then the revisit going empty, other pages not stamping, the baseline holding still while mounted, and the nav row / hard reload).
  • npm run build run and internal/webapp/static committed; frontend/check-dist.sh reports fresh.
  • UI checked by driving the real hub; the screenshots above are from that pass.

One note on the plan: it proposed two PRs (server, then frontend) and said one was fine — this is the one-PR version, since the e2e spec needs both halves anyway. Nothing else deviates.

Build session

cd $(git worktree list | grep bea-65-ph-idea-what-changed-while-i-was-away | awk '{print $1}') && claude --resume 8610a341-f27a-44d9-9d35-e013e37e909b

(only works on the machine this ran on)

🤖 Generated with Claude Code

The change feed is flat reverse-chron with no notion of when you last
looked, so "what happened while I was away" is date arithmetic in your
head — and cheap agents write more files per day than that scales to.

Server: `?since=<RFC3339>` on GET /api/p/<id>/history, one case in the
existing filter switch. It sits after the kinds[] classification (which
must keep walking every op, or a filtered view relabels an edit as an
add) and before the sort, so next_cursor is minted from the filtered
list and paging a since-feed terminates on its own oldest match.

Client: /<project-id>/since — a real VIEW_ROUTES entry — renders the
existing HistoryView with that filter and a header line. The anchor is a
per-(account, project) localStorage marker, read once from a useState
initializer and stamped after entries render: read it any later and the
page empties itself while you are reading it. SinceView is the only
caller of stampVisit, so no other page moves the marker.

Known ceiling: the marker is per browser, not per account — laptop and
phone keep separate last-visit times. The server-side marker is the
larger follow-up.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant