Adds a Places tab to the Photos section: a self-hosted MapLibre GL map of
the user's geotagged photos.
- places.js: lazy-loads vendored MapLibre + pmtiles.js on first open; draws
the server-aggregated clusters (GET /api/photos/geo) as HTML thumbnail
markers (no glyphs/sprites, no client-side clustering); pan/zoom refetches
for the new viewport; click a cluster to zoom in, or a single photo to
open it in the lightbox.
- Optional vector basemap read directly from /basemaps/basemap.pmtiles over
HTTP Range (label-light Protomaps style, light/dark aware); falls back to
a themed background when no basemap is present. ODbL attribution shown.
- "Moments | Places" sub-nav wired into the Photos section.
- enable_places now defaults on, since the route + UI are ready.
No new backend serving code: tower-http ServeDir already serves static/
with Range, so the .pmtiles basemap is just a static file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
Backend — Photos timeline now groups by real capture date instead of upload time. New MediaMetadataService (FileLifecycleHook) extracts EXIF DateTimeOriginal from images and container creation_time from videos (mov/mp4/mkv) via nom-exif, timezone-correct (OffsetTimeOriginal), persisting captured_at so the existing media_sort_date trigger takes over. Adds POST /admin/photos/metadata/reextract to backfill existing media. Falls back to upload date when no embedded date exists.
Frontend — premium grid cards: combined metadata line (relative date · size, owner avatar when shared), custom selection checkbox with a clear checked state, uniform full-width 4:3 thumbnail tiles independent of filename length, centered file-type icons, and a hit-test fix so checkbox/star/kebab clicks reach the controls (the decorative thumbnail no longer captures pointer events). Notification messages internationalised across all 16 locales. Broader polish: design tokens, a11y/focus-visible states, brand + PWA assets.
Chore — bump semver-compatible dependencies (cargo upgrade); add nom-exif 3.6.1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every folder navigation awaited rebuildBreadCrumb() — one sequential
fetch per ancestor level — before requesting the first page, so a
depth-8 folder paid ~8 RTTs of dead time before the content even
started loading. And nothing was cached: navigating between sibling
folders re-fetched the same ancestors every time.
- filesModel: session cache id → FolderItem for breadcrumb resolution
(warm navigations rebuild the trail with zero fetches), invalidated
on rename/move (single + batch) via invalidateFolderMeta. The trail
is now built locally and committed atomically, guarded by a
generation token so a rebuild superseded by a faster follow-up
navigation can no longer interleave writes into the newer trail.
- filesView.loadFiles: the rebuild runs concurrently with the first
page fetch; crumbs, history and title update when it resolves.
Navigation latency becomes max(listing, ancestor chain) instead of
their sum — and ~equal to the listing alone once the cache is warm.
The home-folder fallback for inaccessible targets is preserved by
reloading the listing when the rebuild had to reset app.currentPath.
https://claude.ai/code/session_01Dp3oWon5GBMVn4j3QXZdgx
Four related fixes to stop buffering files in page memory and stop
hammering layout from the selection paths:
- Inline viewer media: video/audio fetched the ENTIRE file into a blob
before the first frame (a 2 GB video = 2 GB of tab heap, no
progressive playback, no seek). The <video>/<audio> src now points
straight at the same-origin API URL — cookies travel automatically
and the browser streams with native Range requests, exactly like the
music player already did.
- Downloads (file, folder ZIP, batch ZIP, viewer button): the
fetch → blob → objectURL pattern materialized the whole payload in
RAM before the save dialog appeared (a 10 GB batch ZIP risked
crashing the tab) with no download progress UI. New shared
utils/download.js hands the URL to the browser, which streams to
disk with its own progress UI. Batch download uses the existing GET
endpoint (same URL contract as the drag-out DownloadURL, now shared
via buildBatchDownloadUrl); selections whose id list cannot fit in a
URL keep the buffered POST fallback. Trade-off: failed downloads now
surface in the browser's download shelf instead of an in-app toast.
- Rubber-band lasso: every mousemove (>100/s) walked all cards
interleaving getBoundingClientRect() reads with class writes — up to
N forced reflows per event, freezing the frame rate on folders with
thousands of loaded rows. Card rects (+ item info) are now snapshot
once per drag (rebuilt on scroll), and a single rAF pass per frame
compares against the cached geometry, touching only cards whose
selection state changed.
- batchToolbar: getSelection() rebuilt the selection by scanning every
.file-item in the document (the TODO admitted it); it now reads the
_selected Map that every selection path already keeps in sync.
clear() scopes its DOM sweep to #files-list (the only container the
toolbar manages) instead of the whole document.
https://claude.ai/code/session_01Dp3oWon5GBMVn4j3QXZdgx
displaySearchResults inserted up to 100 results one at a time via
addItem({ scroll: true, highlight: true }) — each insert ran two DOM
scans (duplicate guard + row lookup), a smooth scrollIntoView and a
highlight pulse, so every live-search keystroke triggered ~100 competing
smooth-scrolls and O(n²) container scans.
Add filesView.renderItems(): a single component.render() pass (one
DocumentFragment, zero per-item scans/scrolls). addItem keeps the
scroll/highlight affordances for its documented purpose — optimistic
single-item inserts after upload/create.
https://claude.ai/code/session_01Dp3oWon5GBMVn4j3QXZdgx
issue: on folder creation view was wiped and displaying only the new folder
fix: add a "new" swimlane if in group mode and scroll up to the new created folder
- OIDC JIT define the locale only at user creation, user can so change his preference later
- invited users will inherit inviter's locale
- email will use prefered_locale
- login to a new browser will use prefered_locale
this implements first version (manageable only by admin right now)
routes:
GET /api/groups
List subject groups (paginated). Admin-only.
POST /api/groups
Create a new ReBAC subject group. Admin-only. The name must match the RFC 5321 local-part shape and be globally unique (case-insensitive).
GET /api/groups/search
Search non-virtual groups by name substring. Authenticated only (no admin role required) — backs the share-dialog recipient autocomplete.
GET /api/groups/{id}
Fetch a single group's details. Admin-only.
DELETE /api/groups/{id}
Delete a group. Cascades to `subject_group_members` (FK) and to `access_grants` rows referencing this group as a subject. Admin-only.
PATCH /api/groups/{id}
Update a group's metadata. Admin-only. v1 only persists name renames.
GET /api/groups/{id}/effective-members
List every user transitively reached through this group (members of members of members, etc.). Used by admin / audit tooling. Admin-only.
GET /api/groups/{id}/members
List the *direct* members of a group (one level only). Admin-only.
POST /api/groups/{id}/members
Add a member to a group. Exactly one of `user_id` / `group_id` must be provided. Adding a group-member runs a write-time cycle check and a nesting-depth check (max 8). Admin-only.
DELETE /api/groups/{id}/members/group/{gid}
Remove a nested group-member from a group. Admin-only.
DELETE /api/groups/{id}/members/user/{uid}
Remove a user-member from a group. Admin-only.
fix hurl
groups
round
groups
- On mobile (≤768px), replace the full search bar in the topbar with a
search icon button; tapping it expands a full-width search overlay
with a back arrow to collapse (Google Drive-style pattern)
- Escape key also collapses the mobile search overlay
- Hide the redundant inline search icon when the mobile search bar is
active (submit button already has one)
- Move the language selector from the topbar into the user menu for all
screen sizes, reducing topbar clutter; it renders as an accordion row
matching other menu items
- Fix user menu positioning in RTL layouts (Arabic/Persian): anchor to
left:0 instead of right:0 so the panel opens inward rather than
off-screen
- protect file_management_service::rename_file with validate_storage_name
- remove specific rename modal and use the generic modal class (less duplicate)
- handle errors on modal action: do not close the modal on error and display this error
- hide "Go to parent folder" contextMenu if section is files and folder is the same as current one
this change replace original window.fetch by a wrapper that check any 401 response, is so it will request a refresh token
this solve current issue with Favorites & Recent sections that give blank page when token is expired
- exclusion of requests to other domain (401 will not be handled here)
- security with shares /api/s is not handled
- check with CSRF, no risk