Commit Graph

1235 Commits

Author SHA1 Message Date
Claude 5494efea35 feat(frontend): port Places, People & photo tabs to the Svelte app
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.
2026-06-19 12:59:16 +00:00
Claude 047e0f06ff Merge main (Photos/People/Places + ReBAC) into the SvelteKit rewrite
Bring the feature-rich main branch into the frontend Svelte rewrite
(PR #478, base bcn/frontend-svelte-rewrite). main moved well ahead of the
PR's branch point (b8a0018): it added the Places photo-map and People
(faces) backends, photos enhancements, the ReBAC→role-grants migration,
load tests, and more.

Conflicts resolved (4 files):
- Dockerfile: combine the explicit --bin allowlist (defence-in-depth from
  main) with the SPA copy from the frontend build stage (PR).
- .github/workflows/ci.yml: keep the PR's Svelte frontend job
  (svelte-check + eslint + stylelint + prettier + vitest); the legacy
  static/-targeted tsc/locale/icon advisory steps don't fit the new
  working-directory: frontend job and svelte-check supersedes them.
- justfile: keep both the new fe-* / dev recipes (PR) and the load-* k6
  recipes (main).
- static/locales: keep the PR's symlink (-> ../frontend/static/locales);
  main's new photos/people locale keys are folded into the Svelte locale
  files alongside the ported views.

Backend (people/places/faces handlers, routes, DI, migrations) merged
cleanly. `cargo check --bins` passes. The new Places/People UI is not yet
in the Svelte app; that is ported in follow-up commits.
2026-06-19 12:45:22 +00:00
Dionisio Pozo 1c9bb30475 Merge pull request #485 from AtalayaLabs/claude/zealous-faraday-58s1at
feat: Photos evolution — Places (map) & People (faces) + gallery polish
2026-06-19 14:31:14 +02:00
Claude f4e387bad0 docs(plan): mark Phase 2 (People) complete with ONNX analyzer
Records the real SCRFD+ArcFace analyzer behind the faces-onnx feature,
the FacesConfig env vars, and the deviation (unit-tested geometry split
from the feature-gated ONNX seam). Notes what stays optional: per-user
consent gate, lightbox face boxes, and a periodic re-cluster scheduler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 12:29:33 +00:00
Claude 12ede47b2c feat(faces): real ONNX face analyzer (SCRFD + ArcFace), opt-in
Implements the last Phase 2 piece: a working face detector/embedder behind
the new `faces-onnx` cargo feature (mirrors how `plugins` gates wasmtime).
Inert by default — the default build is unchanged and ships the no-op
analyzer.

Pipeline (InsightFace/immich pattern): SCRFD detection with 5-point
landmarks → least-squares similarity alignment to the canonical 112×112
template → ArcFace embedding → L2-normalized 512-d vector.

- face_geometry.rs (always compiled, unit-tested): SCRFD anchor/distance
  decode, NMS, the closed-form (complex-number) similarity transform,
  bilinear affine warp, NCHW normalization, L2-norm, Laplacian sharpness.
  11 unit tests cover the error-prone math with no model needed.
- onnx_face_analyzer.rs (feature `faces-onnx`): wires the geometry to ONNX
  Runtime via `ort` (load-dynamic, so libonnxruntime is dlopen'd at runtime
  and the crate builds without it). Inference runs on spawn_blocking; each
  session is serialized behind a Mutex. Loads via `ort::init_from` (fallible)
  not ORT's lazy loader, which would panic under `panic = "abort"`.
- config: FacesConfig + OXICLOUD_FACES_{ORT_DYLIB,DETECTOR_MODEL,
  EMBEDDER_MODEL,DET_SIZE,DET_THRESHOLD,NMS_THRESHOLD,INTRA_THREADS}.
- di: build_face_analyzer() loads the real analyzer when the feature is
  compiled in and runtime+models are configured; any missing piece or load
  failure degrades to the no-op analyzer (logged) so startup never fails.
- ort/ndarray added as optional deps; example.env documents the setup.

Models and the ONNX Runtime dylib are operator-provided at runtime and are
never committed. Cannot be exercised in CI (no models/dylib); the geometry
is unit-tested and the ONNX seam is isolated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 12:28:49 +00:00
Claude eacb913375 docs(plan): record Phase 2 (People) backend + frontend status
Updates the implementation status from "not started" to reflect the
landed face-recognition stack and notes the deviations from the original
plan (BYTEA embeddings + in-Rust cosine instead of pgvector; a single
FaceAnalyzerPort instead of split detector/embedder ports; union-find
connected-components clustering). Flags the remaining piece: the real
ONNX analyzer (ort + operator-supplied models), which adds a crate and
can't be exercised in this environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:59:04 +00:00
Claude 6314fa6b1c feat(people): add People tab frontend for face clusters
Adds the client side of Phase 2 (People). A new "People" tab in the
Photos sub-navigation lists identity clusters from GET /api/people and
drills into a person's photos using the existing photos lightbox.

- people.js: peopleView with list/drill-in/rename, reusing .photos-grid
  tiles and photosLightbox; rename via Modal.prompt + PATCH /api/people/{id}
- people.css: person grid, circular avatars, single-person header,
  loading/empty states — all design tokens, no raw colors
- places.js: People tab wired into the Moments|Places sub-nav, revealed
  only when GET /api/people is reachable (capability probe); _switchTab
  now toggles three views
- index.html: load people.css + people.js
- en.json: photos.tab_people + people.* labels (other locales fall back
  to English via i18n)

The tab stays hidden unless OXICLOUD_ENABLE_FACES is on (the API 404s
otherwise), so this is inert by default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:58:23 +00:00
Claude 5c42b4d2b1 feat(faces): /api/people endpoints
Phase 2 increment 6 — the People HTTP API (mounted only when
OXICLOUD_ENABLE_FACES is on; every handler is caller-scoped):
- GET    /api/people                 list identity clusters
- GET    /api/people/{id}/photos     a person's photo file ids
- PATCH  /api/people/{id}            name / rename a person
- POST   /api/people/{id}/hide       hide / unhide
- POST   /api/people/merge           merge two clusters
- POST   /api/people/recluster       re-run clustering
- DELETE /api/people/data            erase all face data (opt-out)
- GET    /api/people/faces/{file_id} face boxes for lightbox tagging

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:51:01 +00:00
Claude 1dca9d7d05 feat(faces): indexing pipeline + DI wiring
Phase 2 increment 5:
- FaceIndexingService: a FileLifecycleHook that, on image upload, detects +
  embeds faces in a background task and stores them. Dedup-aware (clones an
  identical blob's faces instead of re-running inference), reindexes on
  overwrite, and relies on the DB cascade for deletes. Completely inert when
  no model is ready.
- DI: registers the hook in the FileLifecycleService chain and exposes
  PeopleService in AppState — both gated on OXICLOUD_ENABLE_FACES, both
  using the default no-op analyzer until the operator wires a real ONNX
  model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:46:57 +00:00
Claude de95de013d feat(faces): PeopleService — identity clustering + use cases
Phase 2 increment 4:
- recluster(user): union-find connected-components over the user's face
  embeddings (cosine threshold); groups of >= min_faces become persons, and
  an existing person's name is preserved across reclusters. O(n^2) — fine
  for moderate libraries; an ANN index is the documented scale-up.
- caller_id-scoped use cases for the HTTP layer: list_people (non-empty
  clusters, cover thumbnail, most-photographed first), person_photos,
  faces_for_file, rename, hide, merge, and delete_all (right to erasure).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:40:33 +00:00
Claude 248ad70f01 feat(faces): bytea-backed PostgreSQL repository
Phase 2 increment 3: FacePgRepository implements FaceRepository.
Embeddings stored/read as BYTEA (512 little-endian f32), bbox as REAL[].
Every query is user-scoped. Covers face CRUD, person CRUD (create / rename
/ cover / hide), files-for-person, and a transactional delete-all-for-user
(right to erasure).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:37:17 +00:00
Claude 524e573756 feat(faces): domain entities, ports & no-op analyzer
Phase 2 increment 2:
- domain: Face, Person, BoundingBox, DetectedFace (512-d embeddings).
- ports: FaceAnalyzerPort (detect + embed from raw bytes; decodes internally
  so the application layer stays image/ML-crate agnostic) and FaceRepository
  (user-scoped face/person persistence).
- DTOs: PersonDto, FaceBoxDto.
- NoopFaceAnalyzer — reports is_ready()==false and returns no faces, so the
  whole People pipeline compiles and runs inert until the operator wires a
  real ONNX-backed analyzer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:34:46 +00:00
Dionisio Pozo 1f098b95d8 Merge pull request #484 from AtalayaLabs/claude/youthful-davinci-kvxbse
Batch resource resolution and live-role re-validation on every request
2026-06-19 13:30:11 +02:00
Claude 7cb79d41b9 feat(faces): schema + feature flag for People (off by default)
First slice of Phase 2 (People / faces):
- migration: `faces` schema with `faces.persons` and `faces.faces`.
  Embeddings are stored as BYTEA (512 x f32) — no pgvector extension
  dependency; similarity is computed in-app (pgvector/VectorChord is the
  documented scale-up). Cascade deletes (by user and by source file)
  satisfy the right to erasure.
- OXICLOUD_ENABLE_FACES feature flag, OFF by default (biometric data,
  opt-in per deployment).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:28:39 +00:00
Claude 05d51ededd perf(grants,favorites): batch resource resolution to kill N+1 and pool fan-out
Three list endpoints resolved each resource with one query per id:

- GET /api/grants/incoming and /api/grants/outgoing used
  join_all(ids.map(get_file)) + join_all(ids.map(get_folder)), so a single
  page (limit ≤ 200) could demand ~200 concurrent connections from the
  20-connection primary pool, causing acquire-timeouts and head-of-line
  blocking under load.
- The NextCloud favorites REPORT (oc:filter-files) fetched get_file/
  get_folder once per favorite — up to N serial round-trips per sync.

Add by-ids batch reads that mirror the existing get_file/get_folder column
mapping and NOT is_trashed filter:

- FileBlobReadRepository::get_files_by_ids / FolderDbRepository::get_folders_by_ids
  (one SELECT ... WHERE id = ANY($1)), exposed as FileRetrievalService::
  get_files_by_ids / FolderService::get_folders_by_ids returning DTOs.
- Both grant handlers and the favorites REPORT now issue two batch queries
  total and look results up by id, preserving original order. Missing ids
  (stale grants whose resource was deleted, or trashed/removed favorites)
  drop out exactly as before. No auth-semantics change: these paths already
  resolved ids vetted by the authorization engine / favorites table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAzLEQDaLak3dnrEN3YT35
2026-06-19 11:27:11 +00:00
Claude 5fe6459722 docs(plan): mark Phase 1 (Places) complete via Approach A
Records the as-built choices: static-file basemap via ServeDir Range
(not the pmtiles crate), HTML thumbnail markers (not deck.gl), default-on
flag, and the pending browser smoke-test + operator basemap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:18:33 +00:00
Claude 513b6224b4 feat(places): photo map view (MapLibre + PMTiles, Approach A)
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
2026-06-19 11:17:56 +00:00
Claude bdbb7ae196 fix(auth): revoke JWT access on deactivation, deletion and role change
JWT access tokens freeze role/identity at login (access 1h, refresh 7d)
and validated tokens are cached for 30s. The Bearer and cookie auth paths
trusted claims.role and never re-checked the account, so demoting an admin,
or disabling/deleting an account, did not revoke access until the token
expired. The app-password path already re-read role/active from the DB;
only the JWT/cookie path had the gap.

Re-validate the caller against the live user record on the token path via
the already-cached get_user_flags (role / is_external / active), bounded by
USER_FLAGS_CACHE_TTL and invalidated eagerly on set_user_active /
change_user_role / delete_user_admin:

- middleware/user.rs: new resolve_live_role helper (+ pure decide_live_role
  core) — returns the *current* role, rejects deleted (NotFound) and
  deactivated accounts, and fails open on transient lookup errors (mirrors
  require_internal_user). Login/refresh remain the canonical active gate.
- middleware/auth.rs: auth_middleware (Bearer + cookie) now populates
  CurrentUser with the live role and rejects revoked accounts (Bearer ->
  401 AccountInactive; cookie -> fall through to 401/login redirect).
  require_admin emits an audit line on denial.
- middleware/admin.rs: require_admin / require_authenticated re-check the
  live record, return the live role, and audit admin denials.

Downstream admin gates (dedup_handler, subject_group_handler, OCS) inherit
the live role automatically via CurrentUser / require_authenticated.

Tests: decide_live_role policy (active / demoted / deactivated / deleted /
transient fail-open) and AccountInactive -> 401.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAzLEQDaLak3dnrEN3YT35
2026-06-19 11:13:49 +00:00
Claude bb3d73926a chore(places): vendor MapLibre GL JS + pmtiles.js for the photo map
Vendored for the self-hosted Places map (Approach A: client-side PMTiles
decoding, basemap served as a static file via the existing Range-capable
ServeDir):
- maplibre-gl 5.24.0 (BSD-3) + its CSS
- pmtiles 4.4.1

Also adds static/basemaps/ with a .gitignore (*.pmtiles is operator-provided,
never committed) and a README explaining how to drop in a basemap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 11:10:58 +00:00
Claude f4b431bb03 feat(places): backend for the photo map (geo clusters API)
Phase 1 server side, gated on OXICLOUD_ENABLE_PLACES (off by default):

- migration: partial index on storage.file_metadata(longitude, latitude).
- FileBlobReadRepository::list_geo_clusters — plain-SQL grid aggregation
  (no PostGIS) scoped to the caller's own non-trashed photos, returning a
  centroid, count and a representative file id per non-empty cell.
- PlacesService (caller_id-scoped; user-scoped data needs no authz check,
  mirroring RecentService) with a zoom→cell-size mapping.
- GET /api/photos/geo?bbox=w,s,e,n&zoom=N returning GeoCluster[]. The route
  is mounted only when the Places service is present, and is registered in
  the OpenAPI path list.

The map frontend (PMTiles serving + MapLibre module) is deferred pending
the basemap-sourcing and vendoring decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 10:57:01 +00:00
Claude 3b30a11161 docs(plan): mark Phase 0 complete and record the PhotoDto deviation
Adds an implementation-status section: Phase 0 done (with commit refs),
0.2 implemented via a flattened PhotoDto rather than widening FileDto, the
two Phase 0 loose ends (embedded map pin, drag-marquee, HEIC, sub-nav),
and Phase 1 (Places) marked backend-in-progress.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 10:36:17 +00:00
Claude ca1a8cb8b8 feat(photos): lightbox zoom/pan, mobile swipe & EXIF info panel
- Photos zoom via wheel, double-click and two-finger pinch (1–5x) with
  drag-to-pan; a touch swipe navigates prev/next when not zoomed.
- A new info button toggles a panel showing date, size, dimensions, camera
  and GPS coordinates (resolving the old geoloc TODO), pulled from
  /api/files/{id}/metadata.
- Zoom/pan state resets on every item change and on close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 10:25:33 +00:00
Claude 75ee9b7cc4 feat(photos): justified (aspect-preserving) layout option
Adds a Grid/Justified toggle to the photos toolbar. Justified mode packs
tiles into Flickr-style rows scaled to the container width using each
photo's real aspect ratio (from the new /api/photos width/height, falling
back to 1:1 when missing). It composes with the virtualized renderer:
per-group materialization and the off-screen spacer height estimates are
both layout-aware. The choice persists in localStorage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 10:21:00 +00:00
Claude 8d09589588 feat(photos): expose image width/height on the /api/photos timeline
list_media_files now LEFT JOINs storage.file_metadata and returns each
photo's pixel dimensions next to the sort date. The endpoint wraps FileDto
in a flattened PhotoDto carrying width/height, so the gallery can lay tiles
out at their true aspect ratio (justified layout) without a second per-file
metadata round-trip and without layout shift. FileItem gains optional
width/height. No change to FileDto or its other construction sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 10:15:20 +00:00
Dionisio Pozo f6fb9e27f6 Merge pull request #483 from AtalayaLabs/claude/modest-lovelace-crdw25 2026-06-19 12:07:52 +02:00
Claude 824df4d421 feat(photos): keyboard a11y + shift-click range selection
- Tiles are focusable (tabindex / role=button / aria-label) with a
  :focus-visible ring; Enter opens the lightbox (or toggles in selection
  mode), Space toggles selection.
- Shift-click extends the selection from the last anchor across the
  timeline; the range is tracked in the selection Set so it spans
  dematerialized (off-screen) groups, with visible tiles updated at once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 10:01:19 +00:00
Claude 9a6b174a30 Defer CDC chunk reclamation in manifest dereference to GC
remove_manifest_reference unlinked a chunk's backing file right after the
row-delete committed — the same TOCTOU the GC grace window was added to
close: a concurrent upload of identical content can re-reference (pin) the
chunk in the gap between commit and unlink, after which the deferred
unlink strands a referenced chunk with no bytes.

Route physical chunk reclamation through the single grace-protected path:
on last reference, delete the manifest and decrement its chunks (stamping
orphaned_at on the ones that reach 0), but leave the chunk rows and files
for garbage_collect() to reclaim once orphaned past the grace window. The
manifest deletion and its blob-keyed thumbnail hook stay eager.

remove_legacy_reference and cleanup_if_orphaned's legacy path are left as
eager deletes on purpose: a legacy whole-file hash can never be re-created
by an ingest (uploads are always CDC now), so there is no writer to race —
the existing "row gone ⇒ no resurrection" reasoning holds for them.

Adds an integration test asserting a CDC manifest dereference leaves
chunks orphaned-but-present, then reclaimed by a post-grace GC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172rsVwzTwD216R9HXT2aU4
2026-06-19 09:58:51 +00:00
Claude e8520e485f feat(photos): modern confirm dialog + correct lightbox favorite state
- Add Modal.confirmDialog() (Promise<boolean>, built on openPanel so it
  inherits the overlay, animation, focus-trap and Escape handling) and use
  it to replace native confirm() in the photos batch-delete and lightbox
  single-delete flows.
- The lightbox now reflects the real favorite state when an item opens
  (previously the star always started empty) and toggles favorites through
  the favorites module so its cache stays in sync.
- Add photos.delete_* i18n keys (English; other locales fall back to en).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 09:58:01 +00:00
Claude 44d7e06846 docs(plan): add phased implementation plan for Photos, Places & People
Consolidates the research into a concrete, file-by-file plan following the
repo's hexagonal conventions (AuthZ in services with caller_id, audit logs,
feature flags, sqlx migrations, vanilla JS/CSS):

- Phase 0: gallery polish (virtualization done; dimensions in /api/photos,
  justified layout, lightbox zoom/pan + map pin, a11y, sub-nav).
- Phase 1: Places — MapLibre + self-hosted Protomaps PMTiles served from
  Axum (pmtiles crate) + SQL grid aggregation (no PostGIS).
- Phase 2: People — ort (ONNX) detect+embed, pgvector storage, incremental
  threshold clustering; runtime-downloaded models; opt-in/GDPR.

Includes vendoring/dependency table and the open decisions to resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 09:47:54 +00:00
Claude e0efaed549 Harden blob GC and supervise the content-index worker
Deduplication GC (garbage_collect, Phase 2):
- Add an orphan grace period before a ref_count=0 blob's backing file is
  physically deleted, mirroring git's gc.pruneExpire. New
  storage.blobs.orphaned_at records when a blob last reached ref_count 0;
  the delete trigger and every decrement / 0-ref insert path stamp it,
  every re-reference clears it.
- Cross-check that no manifest lists the chunk and no file points at the
  blob before deleting it (mirrors Phase 1's file check), so a stale
  ref_count can only delay collection, never delete live content.
- Unlink the backing files with bounded parallel fan-out.

Together these close a TOCTOU where a concurrent upload of identical
content could re-reference a chunk in the window between the GC row
delete committing and the backing file being unlinked. Individual file
deletes still reclaim eagerly; only bulk empty-trash and the periodic
sweep observe the grace window.

Trash: match ErrorKind::NotFound instead of substring-matching the error
message when treating an already-deleted item as success.

Content-index worker: supervise the drain loop and restart it with
backoff after a panic, instead of letting a panic silently freeze the
search index while the dirty queue grows unbounded.

Adds migration 20260802000000_blob_gc_grace.sql and an integration test
covering the grace window and reference cross-checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172rsVwzTwD216R9HXT2aU4
2026-06-19 09:37:51 +00:00
Claude 081b2b68d8 perf(photos): virtualize the timeline to bound DOM node count
The photos timeline rendered every tile into the DOM and grew it
unbounded on infinite scroll, degrading on large libraries. Each
date-group is now a <section> whose grid is materialized (tiles
inserted) only while near the viewport and dematerialized (emptied,
height frozen as a spacer) once it scrolls away, driven by an
IntersectionObserver rooted on the scroll container. DOM nodes stay
bounded by a few screens regardless of library size.

- Grouping (day/month/year), infinite scroll, multi-select, video
  thumbnails and fade-in are all preserved.
- Selection state and the video-thumbnail cache survive the
  materialize/dematerialize cycle.
- Falls back to full rendering when IntersectionObserver is unavailable.
- Spacer heights are estimated from grid geometry and re-estimated on
  resize.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
2026-06-19 09:30:41 +00:00
Dionisio Pozo 293986d1f6 Merge pull request #481 from AtalayaLabs/claude/jolly-ramanujan-d2qor5
fix(dav): repair CalDAV/CardDAV client connectivity (#480)
2026-06-19 10:45:03 +02:00
Claude fe852d3b79 fix(dav): repair CalDAV/CardDAV client connectivity (#480)
Standard CalDAV/CardDAV clients (Thunderbird, DAVx5, Apple
Calendar/Contacts) failed to connect, mounted collections read-only, or
could not discover address books, even though curl worked. Three
protocol-compliance gaps caused this:

1. Missing Basic-auth challenge on /caldav and /carddav.
   The 401 returned for these surfaces carried no `WWW-Authenticate`
   header (only /webdav did). Spec-compliant clients never send
   credentials preemptively the way `curl -u` does — they wait for the
   challenge — so Thunderbird never authenticated and failed with
   "discovery failed" / 401. Extend the challenge to all DAV surfaces via
   shared `is_dav_path` / `dav_basic_auth_challenge` helpers.

2. Calendars always advertised read-only.
   The `current-user-privilege-set` write gate compared `owner_id`
   against the literal string "current_user_id", which never matched a
   real UUID, so `<D:write/>` was never emitted and clients mounted every
   calendar read-only. Thread the caller's id through the CalDAV adapter
   and grant write when the caller owns the calendar.

3. CardDAV discovery was incomplete.
   There was no `/.well-known/carddav` route and the root PROPFIND
   exposed neither `current-user-principal` nor `addressbook-home-set`,
   so clients could not locate address books. Add the well-known redirect
   and root/principal discovery responses mirroring the CalDAV adapter.

Adds unit tests for the auth challenge predicate, the calendar
owner/non-owner privilege split, and the CardDAV root/principal discovery
responses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016cVV9nRQjP6G6a8zbNUWMw
2026-06-19 08:38:27 +00:00
Dionisio Pozo 8c325302b4 Merge pull request #451 from EdouardVanbelle/test/prepare-load-and-benchmark-test 2026-06-18 17:01:14 +02:00
Edouard Vanbelle abc75962e0 fix(load-seed): write storage.role_grants instead of access_grants
D-Prep (migration 20260801000002_drop_access_grants) drops
storage.access_grants and replaces it with storage.role_grants — one
row per role assignment instead of N rows per permission bundle. The
load seeder still spoke the old per-permission shape and failed every
nightly with `relation "storage.access_grants" does not exist`.

Both seeder call sites already grant the read bundle (single
permission), which maps cleanly to the `viewer` role; switching them
to the new schema is a one-row INSERT with the role name. The
conflict key drops `permission` since uniqueness is now per
(subject, resource).
2026-06-18 09:58:29 +02:00
Edouard Vanbelle fd467b2d3b ci(load): add ref input to dispatch nightly against any branch
Adds a `ref` input to workflow_dispatch so the nightly can be triggered
from main and run the scenarios against a feature branch that does not
yet carry the workflow file (e.g. feat/drive). Empty input falls back
to `github.ref`, so cron and bare dispatch are unchanged.

The regression-issue title and body now show the tested ref instead of
`github.sha`, which always resolves to the workflow's ref (main) under
workflow_dispatch and would otherwise mislead.
2026-06-18 09:38:25 +02:00
Edouard Vanbelle 0ef4c624c5 chore(load): start implementation of load tests
initial test from Ed's nuc:

    metric                                          pctl  baseline    current     delta    status
    -----------------------------------------------------------------------------------------------
    folder_cascade.list_depth1                      p50   0.3ms       0.3ms       -6.3%    ok
    folder_cascade.list_depth1                      p95   2.3ms       0.5ms       -75.7%   ok
    folder_cascade.list_depth1                      p99   4.7ms       2.5ms       -48.1%   ok
    folder_cascade.list_depth4                      p50   0.4ms       0.3ms       -10.0%   ok
    folder_cascade.list_depth4                      p95   0.9ms       0.6ms       -31.2%   ok
    folder_cascade.list_depth4                      p99   2.4ms       1.0ms       -56.7%   ok
    folder_cascade.list_depth8                      p50   0.3ms       0.3ms       -8.8%    ok
    folder_cascade.list_depth8                      p95   0.6ms       0.5ms       -22.2%   ok
    folder_cascade.list_depth8                      p99   1.9ms       0.5ms       -71.5%   ok
    folder_cascade.list_depth_deep                  p50   0.3ms       0.3ms       -5.0%    ok
    folder_cascade.list_depth_deep                  p95   0.6ms       0.5ms       -18.6%   ok
    folder_cascade.list_depth_deep                  p99   2.0ms       0.7ms       -67.2%   ok
    share_cascade_rebac.list_grants                 p50   0.4ms       0.3ms       -27.6%   ok
    share_cascade_rebac.list_grants                 p95   1.2ms       0.5ms       -57.0%   ok
    share_cascade_rebac.list_grants                 p99   1.7ms       1.1ms       -36.7%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p50   0.5ms       0.5ms       -11.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p95   1.1ms       0.7ms       -41.2%   ok
    share_cascade_rebac.fetch_as_grantee_depth1     p99   3.0ms       1.3ms       -58.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p50   0.5ms       0.5ms       -13.7%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p95   1.4ms       0.7ms       -50.5%   ok
    share_cascade_rebac.fetch_as_grantee_depth4     p99   2.2ms       1.1ms       -49.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p50   0.5ms       0.4ms       -16.0%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p95   0.9ms       0.7ms       -26.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth8     p99   1.5ms       0.9ms       -40.1%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p50   0.5ms       0.4ms       -17.3%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p95   1.0ms       0.7ms       -31.2%   ok
    share_cascade_rebac.fetch_as_grantee_depth_deep p99   1.6ms       0.8ms       -49.4%   ok
    subject_group_nested.fetch_as_member_depth1     p50   0.5ms       0.4ms       -8.4%    ok
    subject_group_nested.fetch_as_member_depth1     p95   0.6ms       0.6ms       -10.2%   ok
    subject_group_nested.fetch_as_member_depth1     p99   1.4ms       0.6ms       -55.2%   ok
    subject_group_nested.fetch_as_member_depth4     p50   0.5ms       0.5ms       -7.9%    ok
    subject_group_nested.fetch_as_member_depth4     p95   0.6ms       0.6ms       -4.0%    ok
    subject_group_nested.fetch_as_member_depth4     p99   0.7ms       0.6ms       -2.2%    ok
    subject_group_nested.fetch_as_member_depth8     p50   0.5ms       0.4ms       -8.9%    ok
    subject_group_nested.fetch_as_member_depth8     p95   0.5ms       0.6ms       +7.1%    ok
    subject_group_nested.fetch_as_member_depth8     p99   0.6ms       0.7ms       +10.3%   ok
    subject_group_nested.fetch_as_member_depth_deep p50   0.5ms       0.4ms       -7.6%    ok
    subject_group_nested.fetch_as_member_depth_deep p95   0.6ms       0.5ms       -11.9%   ok
    subject_group_nested.fetch_as_member_depth_deep p99   0.6ms       0.7ms       +8.9%    ok
2026-06-18 09:38:25 +02:00
Dionisio Pozo 1d0e4d3c42 Merge pull request #465 from EdouardVanbelle/feat/drive 2026-06-18 09:11:11 +02:00
Dionisio Pozo 009ecfab36 Merge pull request #477 from EdouardVanbelle/feat/plugins-fix-ci 2026-06-18 09:10:54 +02:00
Bradley Nelson 4ddadb5d0f update i18n 2026-06-17 22:52:06 -06:00
Bradley Nelson e6279da5ce svelte refactor 2026-06-17 22:30:56 -06:00
Bradley Nelson 89e14f8f9e visual continunity 2026-06-17 22:07:18 -06:00
Edouard Vanbelle c893f9ae29 chore(roles): remove deprecated tools
this tools are were heere to be tracked, no more necessary once migration is done
2026-06-18 02:47:06 +02:00
Edouard Vanbelle 72129af0bd refactor(role): use grant only
- remove permission centric mode
    - finalize migration drop all tables with permissions
    - ensure roles are ENUM (owner is always displayed first)
2026-06-18 02:47:06 +02:00
Edouard Vanbelle cc6f53528b feat(role): reflect ReBAC doc with changes 2026-06-18 02:47:06 +02:00
Edouard Vanbelle ae219b81fc chore(roles): add validation script for migration
script on Ed's sandbox, adapt values accordingly your environment
2026-06-18 02:47:06 +02:00
Edouard Vanbelle f168c4578f feat(roles): prepare migration ReBAC to roles
prepare migration of permission to roles
    this simplify drastically database (permission are now simply defined in code)
    and will permit reuse of the same ReBAC engine to define owners of drives

    mapping:

    ```
        Role::Viewer => &[Permission::Read],
        Role::Commenter => &[Permission::Read, Permission::Comment],
        Role::Contributor => &[Permission::Read, Permission::Create],
        Role::Editor => &[
            Permission::Read,
            Permission::Comment,
            Permission::Create,
            Permission::Update,
        ],
        Role::Owner => &[
            Permission::Read,
            Permission::Comment,
            Permission::Create,
            Permission::Update,
            Permission::Share,
            Permission::Delete,
            Permission::Manage,
        ],
    ```
2026-06-18 02:47:06 +02:00
Edouard Vanbelle 536f1b8198 chore(drive): add plan of Drive impl. for review 2026-06-18 02:47:06 +02:00
Edouard Vanbelle f095baf26b chore(ci): translation and icons will warn only 2026-06-18 02:46:01 +02:00
Edouard Vanbelle e11e172769 feat(plugins): add missing icons in admin section
% ./tools/check-icons.py
    Found 116 distinct FA icon(s) referenced in static/
    Registry has 128 icon(s) in OxiIcons

    4 missing icon(s):
      • clock-rotate-left               used in: static/admin.html  ✓  viewBox=0 0 576 512
      • puzzle-piece                    used in: static/admin.html, static/admin.html, static/admin.html, static/admin.html  ✓  viewBox=0 0 512 512
      • sync                            used in: static/admin.html  ✓  viewBox=0 0 512 512
      • upload                          used in: static/admin.html, static/admin.html  ✓  viewBox=0 0 448 512

    ✓ Added 4 icon(s) to static/js/core/icons.js
2026-06-18 02:13:17 +02:00