Commit Graph

8 Commits

Author SHA1 Message Date
root
edd569d095 feat: share heaps and folders with other users, fix auth and vision pipeline
Sharing:
- New HeapShare and FolderShare models with read/write permissions
- Sharing API router (CRUD for heap and folder shares)
- Heap endpoints accept shared access (photo_ids, add/remove with write)
- Photo list drops user_id filter in shared context, adds owner_username
- Media serving (thumb/original/proxy) falls back to share check on 404
- ShareDialog component for managing shares from kebab menus
- HeapsPanel shows "Shared with me" section for shared heaps
- LeftSidebar shows "Shared with me" section for shared folders
- Owner badge on PhotoThumbnail for photos from other users

Auth:
- Access token default bumped to 1 year, refresh to 10 years
- Refresh token persisted in localStorage (survives page reload)
- Timer-based refresh replaced with 401 axios interceptor

Vision pipeline fixes:
- Bootstrap sets Redis ready key even on partial export failure
- Export functions run conditionally (only for actually missing models)
- _load_thumb handles multi-user path (/data/thumbs/{user_id}/{photo_id}/)
- can_access_photo_via_share uses single subquery instead of N+1 loop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:59:07 +02:00
348e9c3585 feat: multi-user auth with per-user media isolation
Introduce username/password authentication with admin and user roles.
Each user gets their own media directory under /photos/{username}/ with
isolated photos, folders, heaps, and tags. Admins manage users and
observe the full library from a dedicated Settings page.

Backend:
- User model with bcrypt passwords and JWT access/refresh tokens
- Auth router (login, refresh, setup, change-password, status)
- Admin router (user CRUD with last-admin protection)
- user_id FK added to photos, folders, source_roots, heaps, tags
- All data routers scoped by authenticated user
- Scanner inherits user_id from source root owner
- Thumbnails stored under user-prefixed paths for isolation
- Library endpoints accept ?scope=global for admin cross-user view
- Alembic migration 0009 with data migration for existing installs
- Defensive bootstrap.py handles fresh vs existing DB startup

Frontend:
- AuthContext with token lifecycle, auto-refresh, login/logout
- Login page, first-run setup page, auth gate in App.tsx
- Bearer token interceptor on all API requests
- User identity + logout in left sidebar
- Admin-only Settings page with Library Management and Users tabs
- UserManagement panel (add, edit role, reset password, deactivate)
- Settings shows global stats across all users for admin
- Filter bar, right sidebar, keyboard hints hidden on settings page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:47:43 +02:00
696477eefd feat: heap row kebab menu with rename + duplicate
The heap row used to fan out three small icon buttons (set active, convert
to folder, delete) on hover, which crowded the row and didn't leave room
for new actions. Collapse the destructive / occasional ones into a kebab
menu and add the missing operations.

- Right-aligned action cluster: active indicator → count badge → target
  toggle (when not active) → kebab menu, all flex-shrink-0 so the name
  truncates first.
- Kebab menu items: Rename, Duplicate, Move to folder…, Delete. Outside
  click and Escape close the popover; the trigger has aria-haspopup +
  aria-expanded. Delete still confirms via window.confirm.
- Inline rename: double-click a heap row OR pick Rename from the menu
  to edit the name in place. Enter commits, Escape cancels. Mirrors the
  folder rename pattern in LeftSidebar.
- backend: new POST /heaps/{id}/duplicate creates a copy with the same
  membership ("{name} (copy)") via INSERT...SELECT on heap_photos.
  Never marks the new heap as active so duplicating doesn't quietly
  steal the user's T-key destination.
- api.ts: heaps.duplicate wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:36:00 +02:00
bb7c2b12d6 feat: heap convert can create a subfolder under the target
The previous /heaps/{id}/convert dropped photos directly into a chosen
source root, which is rarely what you want — Lightroom-style behaviour
is "make a folder named after the collection inside the library".
Now the dialog lets you do that.

Backend
- HeapConvertBody gains an optional subfolder_name field. Path
  separators and dot-segments are rejected. When set, the handler
  joins it onto the resolved parent_dir, mkdir's it if missing, and
  uses the resulting path as the move/copy destination. Otherwise
  the parent_dir itself is used (unchanged behaviour).
- The Folder DB row for the destination is created via the existing
  scanner get_or_create_folder helper so dedupe + path normalization
  stay consistent across the codebase.
- The target source root id is propagated through both the source-
  root and folder branches so the new Folder row is correctly
  parented when subfolder_name is set on a folder target too.

Frontend
- HeapConvertDialog grows a "Subfolder name" input that prefills
  with the heap name when the dialog opens. Trimmed empty value
  drops directly into the parent. A live hint below the input
  shows exactly which path will be created (or that the parent
  will be used).
- api.ts heaps.convert() signature accepts an optional
  subfolder_name field; the dialog sends it via mutationFn.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:31:14 +02:00
bed817d274 feat: heap convert-to-folder + surface exact-duplicate detection
Two related polish items.

1. Heap convert to folder
   Closes a long-standing TODO from spec §6.10.
   - Backend: POST /heaps/{id}/convert with body
       { target_id, mode: 'move'|'copy', delete_heap: bool }
     target_id resolves either as a Folder id or a SourceRoot id (same
     convention as /photos/move). For each member photo, dispatches
     either shutil.move + photo.folder_id update, or shutil.copy2 +
     a new is_duplicate=true Photo row with all metadata copied. Name
     collisions on copy use the same " (copy N)" suffix scheme as
     /photos/copy. The heap row is optionally deleted on success.
     Per-photo failures are collected into the response instead of
     aborting the batch.
   - Frontend: new HeapConvertDialog with a target-folder dropdown
     (currently from sourceFolders.list, sub-folder picking is a
     follow-up), move/copy radio, and a "delete heap" checkbox.
     HeapsPanel rows get a hover FolderOutput button that opens it.
     Toast on success names the verb + count and notes whether the
     heap was deleted; invalidates heaps + photos + folders queries.

2. Surface exact-duplicate detection
   The scanner already sets Photo.is_duplicate=true when a SHA-256
   match is found, but nothing surfaced it. Now:
   - Backend list_photos accepts an optional is_duplicate query
     param so the frontend can filter duplicates-only views.
   - filterStore gains a duplicates: boolean field with setter, URL
     sync (?duplicates=true), filtersToParams entry, and a
     hasActiveFilters check.
   - LeftSidebar gets a new "Duplicates" library node (Copy icon)
     that clearAllFilters() + setDuplicates(true). isItemActive
     follows the filter so the highlight stays in sync after
     external filter changes.
   - PhotoThumbnail renders a small dark badge with the Copy icon
     bottom-right when photo.is_duplicate. Sits next to the existing
     basket / discard badges so the user can spot duplicates at a
     glance.
   - Photo TS type adds is_duplicate.

Perceptual-hash duplicate detection (re-encoded / resized matches)
is intentionally a follow-up — needs an imagehash dep, a phash
column, a backfill job, and similarity-search endpoint with
hamming-distance grouping. This commit only surfaces what the
scanner already finds via byte-level SHA-256 comparison.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:28:18 +02:00
351ccd7bb4 refactor: unify Pick with active heap membership
Pick and add-to-heap were two ways of saying "I want to keep this
one". Merging them: P now toggles the selection's membership in the
active heap. The is_picked flag goes away (orphaned in the DB the
same way is_trashed was).

Backend
- Drop is_picked from PhotoBase / PhotoUpdate / PhotoResponse and
  from the photos list filter param.
- Drop the is_picked Column from the Photo model (DB column stays
  on legacy installs but is no longer read or written).
- Drop the bulk action 'pick' branch.
- New GET /heaps/{id}/photo_ids returns just the flat string list.
  Used by the frontend for fast client-side membership lookups
  without fetching full photo records.

Frontend
- New hooks/useActiveHeapMembersQuery.ts → returns
  { activeHeap, memberIds: Set<string> }. Subscribes once at the
  Timeline level and passes a derived isInActiveHeap bool down to
  each PhotoThumbnail (avoids hundreds of thumbnails subscribing
  to the same query).
- PhotoThumbnail: replaces the old check-icon Pick affordance with
  a clear basket badge in the bottom-right corner — a small filled
  pick-colour pill containing a ShoppingBasket icon — visible only
  when the photo belongs to the active heap.
- P shortcut (useKeyboardShortcuts) now toggles membership: if every
  selected photo is already a member, it removes them; otherwise it
  adds the missing ones. T binding removed (P fully replaces it).
- RightSidebar Pick button is now a Pick / Picked toggle bound to
  the active heap. Disabled with a hint when no heap is active.
  Shows the heap name in its title attr.
- filterStore drops 'picked' and 'unflagged' from FlagFilter.
  FilterBar's flag dropdown is now just Any / Discarded.
- LeftSidebar drops the "Flagged" virtual node (it just set
  flag=picked, which no longer exists).
- KeyboardHints: P → "Pick → heap".
- Photo TS type drops is_picked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:20:31 +02:00
ebae775f3f feat: heaps end-to-end with active heap and T shortcut
Adds the spec §6.10 heaps concept: named photo collections with a
single "active" target for fast keyboard adds. Uses a basket icon
(ShoppingBasket) to visually distinguish heaps from folders.

Backend (routers/heaps.py)
- Replaces the 27-line stub with full CRUD: list (with photo counts
  via a single LEFT JOIN), create, patch (rename + set active), delete.
- Add/remove photos endpoints with idempotent semantics: re-adding an
  existing member is a no-op, removing a non-member is a no-op.
- Setting is_active=true on one heap clears the flag on every other
  heap in a single UPDATE so we maintain the single-active invariant.
- routers/photos.py list endpoint now applies the heap_id filter via
  IN-subquery against heap_photos (it was a declared param but had
  no filter logic).

Frontend
- New hooks/useHeapsQuery.ts and useFilterUrlSync wires heap_id as
  another URL-persisted filter; usePhotosQuery threads it through.
- New components/heaps/HeapsPanel.tsx replaces the LeftSidebar Heaps
  stub. Shows the basket icon, photo counts, lets you create heaps
  inline, click to filter the timeline, set active via the target
  icon, and delete heaps.
- TopBar shows an "active heap" pill (basket + name) so the user
  always knows where the next T-press will land.
- KeyboardHints adds T → Add to heap.

T shortcut (useKeyboardShortcuts)
- Reads the active heap from the heaps query cache and the selection
  from the photo store at fire time. Adds the selected photos (or the
  active photo if nothing is selected) via POST /heaps/{id}/photos.
- Toasts:
  - "Added to {heap}: N photos (M already present)" on success
  - "No active heap" hint when none is set
  - "Nothing selected" hint when there's no selection or active photo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:06:52 +02:00
46a0d7aba8 feat: structure 2026-04-06 23:30:19 +02:00