Skip to content

feat(web): web UI v2 — bulk ops, trash, export#38

Merged
paperhurts merged 18 commits into
mainfrom
feat/web-ui-v2
May 26, 2026
Merged

feat(web): web UI v2 — bulk ops, trash, export#38
paperhurts merged 18 commits into
mainfrom
feat/web-ui-v2

Conversation

@paperhurts
Copy link
Copy Markdown
Owner

Summary

  • Multi-select on the list page + BulkActionsBar (add tag, remove tag, set folder, move to trash, clear). One commit per bulk action.
  • /trash route with restore. Filters deleted_at != null within the 30-day GC window.
  • Netscape HTML export from every page (Blob download).
  • Two new pure helpers in @gitmarks/core: updateBookmarks (bulk) and restoreBookmark.

Closes #25.

Test plan

  • 105 web tests + 71 core + 96 extension-shared = 272 total green
  • Full monorepo typecheck + build clean
  • Manual smoke test (see packages/web/README.md v2 checklist)
  • Exported gitmarks.html imports cleanly into Chrome / Firefox

Implementation notes

  • Plan: docs/superpowers/plans/2026-05-25-gitmarks-web-ui-v2.md (13 tasks, subagent-driven with per-batch review).
  • Bulk mutators are pure factories: closure captures ids + tag/folder/nowIso; the produced mutator re-reads file.bookmarks from its argument, so 409 retry-replay against fresh data stays correct.
  • Selection clears on success; preserves on failure for retry.

🤖 Generated with Claude Code

paperhurts and others added 18 commits May 25, 2026 19:24
…ions

Adds two new exports to @gitmarks/core: `updateBookmarks` for applying
patches to multiple bookmarks in a single atomic operation, and
`restoreBookmark` which composes `updateBookmark` to clear `deleted_at`.
Both are pure functions suitable for use as GitHubClient.update() callbacks.
Exposes `BookmarkPatch` interface from the public index. Test count: 65 → 71.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend BookmarkRow with optional selected/onToggleSelect props and
BookmarkList with selected/onToggleSelect/onSetAll props; checkbox UI
is only rendered when onToggleSelect is provided, keeping the legacy
call-site (no selection props) fully unchanged. 3 new tests → 83 total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… clear)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires useSelection + BulkActionsBar into ListPage, passing selected/onToggleSelect/onSetAll to BookmarkList. Adds two integration tests (91 web total). Also fixes BookmarkList select-all header counter text to avoid DOM ambiguity with BulkActionsBar's "N selected" label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds deletedBookmarks helper, TrashList component, TrashPage route, and
wires /trash into the hash router with single-item restore via bulkRestore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e + render

Add isSafeBookmarkUrl() to @gitmarks/core with an explicit allowlist of
tab-navigable schemes (http/https/mailto/ftp/chrome/about/moz-extension/
chrome-extension/view-source). BookmarkRow renders unsafe URLs as a plain
italic span instead of a clickable anchor. bookmark-factory throws on any
unsafe URL so the popup save-flow surfaces the error before a bad URL ever
reaches the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TagChip now checks rawColor against /^#[0-9A-Fa-f]{6}$/ before using it
in the inline style object. Any value that doesn't match (e.g. a CSS
injection payload like "red; background: url(…)") falls back to the default
#475569, so React never serializes untrusted content as raw CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the generic readOrEmpty<T> helper with two typed functions—
readBookmarksOrEmpty and readTagsOrEmpty—each of which runs safeParse
after client.read returns. A schema failure (e.g. malformed attacker
commit) throws, propagates to loadInitial's catch block, and surfaces
as an error string instead of poisoning the in-memory state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
encodeURIComponent() guards against owner/repo values containing slashes
or other special characters that would otherwise corrupt the API path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@paperhurts
Copy link
Copy Markdown
Owner Author

Post-merge security audit findings + fixes

Ran an Opus security audit on the project (per Sid's request). Three Critical findings; all fixed on this branch before merge.

Critical fixed:

  • javascript: / data: URL clicks could exfil the PAT from localStorage. Added an allowlist (isSafeBookmarkUrl in @gitmarks/core); BookmarkRow renders unsafe URLs as plain spans (italic, magenta, no anchor); the extension's buildBookmark factory throws on save. 8e3f875
  • TagChip interpolated untrusted color strings into a CSS style object (UI-redress / exfil via background-image:url()). Now regex-validated against ^#[0-9A-Fa-f]{6}$ with a fallback. dfd1ba9
  • useGitmarksData blind-cast remote JSON to BookmarksFile/TagsFile. Now Zod-re-validates after client.read; schema failure surfaces as an error rather than rendering attacker payloads. c30ac97

Important fixed:

  • GitHubClient.contentsUrl did not URL-encode owner/repo. Now does. d1e9dd7

Deferred to follow-ups:

  • CSP meta tag on web UI + explicit content_security_policy.extension_pages (defense-in-depth)
  • "Sign out / clear local data" affordance in web UI
  • README guidance on PAT lifecycle when uninstalling the extension

Test impact:

  • core: 71 → 77 (allowlist tests)
  • web: 105 → 108 (unsafe-URL render, malformed color fallback, schema-validation error)
  • extension-shared: 96 → 98 (factory rejection cases)
  • Total: 272 → 283 unit + component tests, typecheck clean, all 5 packages build.

@paperhurts paperhurts merged commit 4080874 into main May 26, 2026
1 check passed
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.

Build: Web UI v2 — bulk operations + trash + export

1 participant