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>
Three targeted frontend fixes:
1. pdf.js smart preload (thumbnail.js, resourceIcon.js). The first PDF
thumbnail of a session stalled 1-2s on the lazy import of the ~1.3 MB
pdf.js stack. buildResourceIcon() now fires thumbnail.preloadPdf()
the moment a PDF row enters the DOM, warming both the module
(~300 KB, via the now promise-memoized getPdfjsLib, shared with real
users) and the worker script (~1 MB, via a cache-priming fetch —
pdf.js only requests it on first getDocument). Only folders that
actually contain PDFs pay the download; idempotent after first call,
resets on failure so transient offline retries.
2. Admin migration poll cleanup (admin.js). The 2s setInterval kept
hitting the API and updating hidden DOM after leaving the Storage
tab, and polled a failing endpoint forever after session expiry
(!resp.ok returned without clearing). New stopMigrationPolling()
helper, invoked on tab switch away from Storage, on non-running
status, and on failed polls; tab re-entry re-arms via loadStorage().
3. resourceList.js render hoisting. Per-row i18n.t() type-cell lookups
and the fully item-invariant _renderCustomActions() HTML were
recomputed for every row; they now resolve once per batch via
_buildItemLabels() (per-category labels memoized, rebuilt each
batch so locale switches keep working). _findLaneByKey() swaps the
container-wide attribute querySelector for an O(1) _lanes Map kept
in sync at the only lane create/wipe sites.
https://claude.ai/code/session_0193Hff42gaA962wThxMGSd1
- add coalesced protection to avoid mail bombing if an invited goes many grant in a short period
- add resentd method in share menu item (work for both internal and external users)
- user can disable email notification via his properties
- add env variable from admin to disable notifications
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
- Add src/application/dtos/cursor.rs with three shared types:
· PageCursor trait — default base64url+JSON encode/decode; one bare
impl line per cursor struct
· CursorQuery struct — standard limit/cursor/sort_by query params with
limit_clamped() and decode_cursor<C>() helpers; compose via flatten
· CursorListResponse<T> — standard {items, next_cursor?} envelope with
from_oversized() and with_cursor() builders
- Migrate GrantCursor to impl PageCursor (remove duplicate encode/decode)
- Update GET /api/grants/incoming/resources:
· SharedWithMeQuery now embeds CursorQuery via #[serde(flatten)]
· Replace file/folder nullable pair with ResourceContentDto (untagged
enum) under a single always-present 'resource' field
· SharedWithMeDto is now a type alias for CursorListResponse<SharedWithMeItemDto>
· Handler uses q.paging.limit_clamped() and decode_cursor<GrantCursor>()
- Add docs/architecture/resource-listing.md — authoritative contract for
all listing endpoints (cursor design, SQL keyset WHERE, sort_by naming,
Rust + JS skeletons, compliance table, migration guide)
- Register doc in VitePress sidebar and architecture index
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the placeholder with a gallery rendered by publicShare.js
against the new public endpoints.
- Grid / list view toggle (localStorage)
- Image thumbs and lazy-loaded video posters
- Click-to-open lightbox with Esc / arrow nav
- ZIP download for the current folder
- Subfolder navigation via URL hash, History API for back
Adds five overlay-related tokens to base/variables.css for the
lightbox surface (on-overlay text, translucent button, drop shadow).
share.html itself only adds an icons.js module import and empties
#share-folder for JS to populate.
- change worker: do not cache html pages (not necessary)
- remove use of window.XXX and maximize import/export,
this will provide more clarety,
show circular dependencies + you will benefit IDE help
Share links now point to /s/{token} (was /api/s/{token}) and render a
proper HTML page instead of raw JSON.
Changes:
- static/share.html: standalone public share page
- static/css/views/share-public.css: share page styles
- static/js/views/public/publicShare.js: client-side logic that fetches
share metadata via /api/s/{token}, handles password-protected shares,
and renders file download / folder info
- build.rs: include share.html in the HTML embed pipeline
- web/mod.rs: serve /s/{token} route (unauthenticated)
- share_dto.rs: generate URLs as /s/{token} instead of /api/s/{token}
- share_handler.rs: new download_shared_file() handler that validates the
share token and streams file content without requiring authentication
- routes.rs: mount GET /api/s/{token}/download (public, uses AppState)