Skip to content

feat(webapp): history can be narrowed by path, author and date (BEA-67) - #113

Merged
ssowonny merged 3 commits into
mainfrom
bea-67-ph-scan-bug-history-has-no-filter-search-date-range-or
Aug 3, 2026
Merged

feat(webapp): history can be narrowed by path, author and date (BEA-67)#113
ssowonny merged 3 commits into
mainfrom
bea-67-ph-scan-bug-history-has-no-filter-search-date-range-or

Conversation

@ssowonny

@ssowonny ssowonny commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

TL;DR

  • History was a flat scroll — no filter, no search, no date range. Fine at 11 rows, useless after a month of agent writes.
  • Now there's a filter bar: path contains, author, date range. It narrows the whole feed, not the page you can see.
  • Filters live in the URL, so a narrowed feed is a link you can send, survives reload, and Back undoes it.
  • API gained q/user/since/until on GET /api/p/<id>/history — useful on its own, paging under a filter still walks every match.
  • Known gap: export is still missing (deliberately its own issue — the complaint here was the unreadable scroll).

Before / after

Before After
before after

Filtered, and the no-match state that offers a way back out:

?q=runbook ?q=no-such-file
filtered no match

On a phone the bar wraps to two rows and every control grows to a 44px tap target:

Before (390px) After (390px)
mobile before mobile after

The API

GET /api/p/<id>/history takes four new params, all optional, all composing with each other and with the existing path=/prefix= scoping:

Param Meaning
q= case-insensitive substring of the path
user= exact account (op.User)
since= inclusive lower bound — RFC3339, or a bare YYYY-MM-DD read as a UTC day
until= inclusive upper bound — a bare date includes the whole day

The filters are applied in the same walk as path/prefix, before the sort and before the cursor skip. That is the whole reason paging needed no new machinery: next_cursor keeps meaning "the next matching entry". Filtering after the skip would silently drop rows, which is what TestHistoryFilterPaging exists to catch.

Two smaller decisions:

  • kinds[] is still computed over every op before any filter, so a filtered view never relabels an edit as an add.
  • since > until is not an error — it means "nothing", and returns nothing. A malformed date is a 400, matching the existing invalid n / invalid cursor posture.

The filter bar

  • State is Route.filters, parsed from the query string of the existing history route (/<project>/history[/<path>]?q=&user=&since=&until=). Navigation is a push, so Back undoes a filter like any other navigation.
  • Because the query string is already part of the TanStack query key, changing a filter resets paging for free.
  • The author <select> is built from the accounts seen in the loaded feed and accumulates — rebuilding it from the current feed would leave a reader who filtered by one author with only that author to pick from, and "Anyone" as the only way out.
  • Dates are bare YYYY-MM-DD and the server reads them as UTC days. The bar says UTC out loud rather than converting silently, which would drop an evening's changes for anyone east of UTC.
  • Zero matches gets "No changes match these filters." with a Clear filters button; the bare "No history yet." stays for an unfiltered empty feed.
  • The bar renders for all three shapes — whole project, folder subtree, and a single file's version list.

Deviation from the plan

The plan wanted the author control to also accept free text for an account not in the loaded window. Shipped as a plain <select> instead: a select is discoverable where a datalist is not, and the accumulating list plus a hand-written ?user= in the URL (which the bar then shows as the selection) covers the same ground without a second input. Say the word if you want the free-text field back.

Architecture changes

architecture/webapp-frontend.md: router gains Route.filters and historyFilterQuery/hasHistoryFilters, and urlForView gains a fourth filters argument; components gains HistoryFilters. No relationships changed. webapp-server.md is untouched — it doesn't draw the history handler, and no Go type changed shape.

Drive-by in the same file (second commit): the escaped quotes in the NewProjectDialog note broke the whole classDiagram, so webapp-frontend.md has been rendering as an error box rather than a diagram. Same on origin/main; fixed here because it is the file this PR edits and the fix is one line.

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

flowchart TB
    Browser["Browser"]
    router["<div style='text-align:left'><b>router</b><br/>+VIEW_ROUTES dashboard history install settings<br/>+LEGACY_VIEWS insights to dashboard<br/>+top-level routes orgs billing<br/>+parseRoute(url, mode) Route<br/>+Route.version ?v= sha, one past version<br/>+Route.trailingSlash notes/ resolves, then replaces to notes<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +Route.filters q user since until, history feed</span><br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +historyFilterQuery(filters) / hasHistoryFilters</span><br/>+urlForPath(path, projectId, version)<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +urlForView(view, projectId, target, filters) / encodePath / decodePath</span><br/><span style='background:#ef444455;padding:0 4px;border-radius:3px'>❌ <s>+urlForView / encodePath / decodePath</s></span></div>"]
    components["<div style='text-align:left'><b>components</b><br/>FileView FolderListing FileTree<br/>HistoryView HistoryRow <span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ HistoryFilters</span> DiffView VersionBanner<br/>Insights ShareDialog NewProjectDialog<br/>ShareBanner SharesTable AdminTable<br/>OrgAdmin HubSettings ProjectSettings<br/>Palette shell AccountBar ...</div>"]
    nav["nav"]
    Note["HistoryFilters drives the SERVER<br/>(?q= ?user= ?since= ?until=), never the loaded page:<br/>filtering what is on screen would lie about<br/>everything below the fold and break next_cursor.<br/>Its state is Route.filters, so a narrowed feed is<br/>linkable, survives reload, and Back undoes it."]
    Browser --> router
    Browser --> components
    components --> nav
    components -.- Note
    classDef added fill:#22c55e22,stroke:#22c55e,stroke-width:2px
    classDef removed fill:#ef444422,stroke:#ef4444,stroke-width:2px,stroke-dasharray:4 3
    classDef noteBox fill:#88888822,stroke:#888888,stroke-dasharray:2 2
    class Note noteBox
Loading

Testing

  • go test ./... — all pass. New: TestHistoryFilters (each param alone, case-insensitivity, both-ends-inclusive dates, RFC3339 bounds, every composition, composition with path=/prefix=), TestHistoryFilterPaging (n=2 + cursor walks the whole filtered set, no repeats, no gaps), TestHistoryBadDateRange (400, and no feed body).
  • npm run e2e — 113 passed. New e2e/history-filters.spec.ts covers filter → narrowed feed → reload keeps it → Back restores → Clear empties the query string, the no-match empty state, the author dropdown, and the folder/per-file shapes.
  • npm test (router round-trip), go vet ./..., ./check-dist.sh — clean.
  • The e2e seed gained a second author (bob@x.io on one file) so the author filter has something to exclude.

Build session

cd $(git worktree list | grep bea-67-ph-scan-bug | awk '{print $1}') && claude --resume ea3398de-48fc-4cc7-97aa-517b0fe9b905

(only works on the machine this ran on)

ssowonny and others added 3 commits August 2, 2026 20:21
The project History view was a flat scroll with no controls but per-row
restore/open/download. Fine at eleven rows; unreadable after a month of
agent writes, and agents write far more than people do.

Four reader filters on GET /api/p/<id>/history — q= (case-insensitive
substring of the path), user= (exact account), since=/until= (UTC bounds,
inclusive at both ends, RFC3339 or a bare YYYY-MM-DD). They compose with
each other and with the existing path=/prefix= scoping, and they are
applied in the same walk as path/prefix — BEFORE the sort and the cursor
skip — so next_cursor keeps meaning "the next matching entry" and paging
under a filter needed no new machinery. kinds[] is still computed over
every op, so a filtered view classifies edits the same as the full feed.
A malformed since/until is a 400, not a silently unfiltered feed.

The filter bar drives those params through the URL rather than component
state: a narrowed feed is a link you can send, it survives reload, and
Back undoes it. The author list accumulates across fetches — rebuilding
it from the current feed would strand a reader who filtered by one
author with only their own name to pick from. Zero matches gets its own
empty state with a Clear.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The escaped quotes inside the NewProjectDialog note made the whole
classDiagram fail to parse — it has been rendering as an error box, not a
diagram, since that note landed. Same breakage on origin/main; noticed
while adding HistoryFilters to the same file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	internal/webapp/static/assets/index-C2ypx3jZ.js
#	internal/webapp/static/assets/index-CO632k9V.css
#	internal/webapp/static/index.html
@ssowonny
ssowonny merged commit c8ab550 into main Aug 3, 2026
2 checks passed
@ssowonny
ssowonny deleted the bea-67-ph-scan-bug-history-has-no-filter-search-date-range-or branch August 3, 2026 05:11
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