The /api/activities list endpoint serialised each row by calling
tagsFor, heartsFor, viewerBookmarked, and ownerAttribution — that's
~5 queries per row. For a list of N activities the endpoint issued
5N queries; for a hundred-row list, hundreds of round-trips.
Add buildBulkLookups(rows, viewerId) that runs one IN-query per
concern (tags, heart counts, viewer-hearted, viewer-bookmarked,
owner attribution) and returns precomputed maps. serialize() now
accepts an optional bulk arg; single-row paths (GET /:id, POST,
PATCH, heart/bookmark toggles) pass undefined and keep their per-row
helpers — fine for one row, wrong for a list.
Also add bulkTagsFor() to server/tags.ts as a reusable helper, and
apply the same treatment to server/users.ts (public-list endpoint
had a 2N pattern on tags + heart counts).
Surfaced by /audit simplify.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>