The folder listing now carries the favorite/share badge state for exactly the
items it returns, so the files browser stops fetching favorites and outgoing
shares separately. This removes the last per-navigation badge round-trips AND
fixes the correctness hole of the previous approaches: badges were derived from
only the first 200 global favorites / shares, so a favorited or shared item
outside that window showed no badge. Now every listed item is correct, and the
work is scoped to the items on screen.
Backend (`GET /api/folders/{id}/listing`):
- `FolderListingDto` gains `favorite_ids` and `shared_ids` (sorted) — listing-
level metadata, so no churn to the many FileDto/FolderDto constructors.
- The handler computes both with two batched, index-backed queries run
concurrently: `FavoritesService::favorited_ids` (auth.user_favorites, ANY) and
`PgAclEngine::shared_resource_ids` (storage.role_grants by granted_by + ANY,
which already covers public links as 'token' grants — same membership the
/grants/outgoing/resources endpoint exposes). Both fold into the ETag.
- Public-share browsing passes empty sets (anonymous, read-only context).
Frontend:
- `listFolder` reads `favorite_ids` / `shared_ids`; the files view seeds local
badge sets straight from the listing and updates them optimistically on
favorite toggle / batch / share creation (via ShareDialog's `onshared`).
- Removes the session `badges` store + its fetches entirely — the listing is now
the single, authoritative, fetch-free source.
Net: favorite/share badges cost zero extra client requests per navigation and
are correct regardless of how many favorites/shares the user has. Validated:
cargo check + clippy -D warnings (backend; integration tests need Postgres,
unavailable here), frontend npm run check + unit tests, and a headless render of
the real files route (list + grid) with the new flags present — no errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8Vb9QHmLZnEMzHz7MrFy6
The merge brought in main's ReBAC→role-grants migration, which changed the
grant contract the Svelte sharing UI (branched before it) was written
against: GrantDto dropped `permission` and now carries an explicit `role`
(owner/editor/viewer/commenter/contributor), and the "admin" role was
renamed "owner". Left unchanged, the share UI derived roles from a
now-absent `permission` field and showed every member as "viewer".
- grants.ts: ShareRole is now viewer|editor|owner; Grant carries `role`
(not `permission`); `roleFromPermissions` → `displayRole`, which collapses
the unexposed commenter→viewer and contributor→editor.
- ShareDialog.svelte: read each subject's role directly (role-grants emits
one row per subject); role picker exposes Owner instead of Admin.
- shared/+page.svelte (My Shares): same owner rename; role badges run
through displayRole so server-only roles render sensibly.
Create/update already POST `role`, so only the read/display path and the
role literal needed fixing. npm run check, test:unit (36) and build pass.
Bring the Photos/People/Places UI that main added (in the legacy vanilla
frontend) into the SvelteKit rewrite, wired to the now-merged backend
(/api/photos/geo, /api/people/*).
Photos page (routes/photos/+page.svelte):
- Moments | Places | People sub-tabs (the People tab appears only when the
faces feature is enabled, via a /api/people capability probe), mirroring
the vanilla photos sub-nav.
- Square ↔ justified layout toggle. Justified uses a Flickr-style
row-packer over the width/height the photos list endpoint returns
(PhotoItem), falling back to 1:1 when dimensions are missing.
New components:
- PhotoLightbox.svelte — the lightbox extracted from the photos page into a
reusable component (items + bindable index, onDelete callback) so the
grid, People and Places all share one implementation (no duplication).
- PlacesMap.svelte — MapLibre GL map with server-clustered markers; the
vector basemap is optional (probed at /basemaps/basemap.pmtiles, themed
fallback otherwise). Cluster click zooms in or opens the lightbox.
- PeopleView.svelte — identity-cluster grid → per-person photo grid, with
rename via the in-app prompt dialog.
Supporting:
- api/endpoints/people.ts (+ peopleEnabled probe); photos.ts gains
fetchPhotosGeo + GeoCluster + PhotoItem; fileThumbnailUrl takes a size.
- lib/vendor/maplibre.ts — minimal typings + lazy loader for the vendored
MapLibre GL + pmtiles globals (kept any-free for ESLint).
- utils/media.ts — shared isVideo / photoTimestamp / minimalPhotoItem.
- Vendored maplibre-gl 5.24.0 + pmtiles 4.4.1 under static/vendors and an
optional static/basemaps dir, matching the PR's vendored-asset pattern.
- New photos.tab_*/layout_*/map_* + people.* keys in en.json.
Verified: npm run check (svelte-check + eslint + stylelint + prettier),
npm run test:unit (36 pass), and npm run build all green.