Commit Graph

145 Commits

Author SHA1 Message Date
6457dc9da5 fix: bottom padding on views so last row clears keyboard hints pill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:51:00 +02:00
38d975e354 feat: highlight matched metadata on thumbnails during search
Renders a thin top-of-cell banner on each photo while a text search
is active, labelling which metadata field matched (filename, title,
note, tag, or EXIF key name) and showing a short excerpt with the
exact matched substring highlighted in amber. Extends the list
endpoint's search to also match photos whose tag names contain the
query so tags show up alongside filename/title/notes/EXIF hits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:38:27 +02:00
64e4ea8083 feat: floating capture-date chip on timeline scrollbar
Tracks the scroll position and shows a small date chip pinned to the
right edge of the timeline, fading in while the user scrolls and out
700ms after they stop. Only active in date-sorted views — other sort
modes hide it since the label would be meaningless. A cached
row-offset/date index keeps the lookup to a single binary search per
scroll frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:01:52 +02:00
dcf6c11a22 refactor: fold date-warning filter into Flag pill
Rolls the standalone "Date issues" toggle back into FlagFilter as a
third value ('any' | 'discarded' | 'date_warning') so the date-warning
control lives in the same popover as Discarded, where operators expect
all flag-style filters. Drops the redundant dateWarning boolean and
its URL param.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:54:35 +02:00
30d03d8d4d feat: editable taken_at + folder-based date repair and filter
Lets operators fix corrupted capture dates at scale. Adds an editable
Date Taken field with a folder/filename-derived suggestion hint, a bulk
Date Taken section in the multi-select sidebar that either applies one
date to the whole selection or infers a per-photo date from each path,
a warning badge on thumbnails whose stored date disagrees with the
path, and a "Date issues" filter pill so suspicious photos can be
surfaced and fixed as a group. Edits are written back to EXIF on disk
so rescans don't clobber the fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:48:55 +02:00
root
339e1be510 feat: hide-from-views flag on folders
Adds a per-folder "hide from views" toggle so noisy subtrees
(screenshots, WhatsApp dumps, work archives) can be excluded from
cross-cutting views without losing indexing. Photos under a hidden
folder are still scanned, thumbnailed, embedded, OCR'd, face-
extracted — they just stop appearing in All Photos, Rated, Colors,
Map, Tags, People, Search, Duplicates, and the sidebar counts.
Navigating directly into the folder still shows every photo.

Schema (migration 0007_folder_hidden):
- folders.is_hidden   user-set toggle, default false
- photos.is_hidden    denormalized effective flag (true iff any
                      ancestor folder is hidden), indexed so cross-
                      cutting queries stay on the existing planner
                      paths

The denorm is maintained by two paths:
- The scanner walks the ancestry chain on insert, with a per-scan
  memoized cache so each folder is resolved once per scan.
- POST /api/v1/folders/{id}/hide flips folders.is_hidden and runs a
  WITH RECURSIVE CTE to recompute every folder's effective state in
  one query, then bulk-updates photos WHERE IS DISTINCT FROM. Runs
  in ~10 ms on a 13k-photo library.

Filters added (cross-cutting queries):
- /library/stats — every sidebar badge via a shared `visible` filter
- /photos (list) — only when neither folder_id nor heap_id is set;
  folder browse and heap browse always show everything
- /photos/map
- /library/duplicates/groups
- /folders/tree photo_count subquery
- /tags count_subq (drives Tags + People sidebar counts)
- services/duplicates.regroup_duplicates (so hidden dupes never
  contaminate the Duplicates view)
- services/search.hybrid_search — both semantic (pgvector) and FTS
  legs join photos so rankings don't include hidden results

Intentionally NOT filtered:
- /photos?folder_id=X and /photos?heap_id=X (user-intentional browse)
- /library/maintenance/pipeline-stats (tracks real worker state)
- cleanup service (disk-level ops, not views)

Frontend:
- sourceFolders.setHidden(id, hidden) API client method
- FolderTreeNode.is_hidden carried through the tree into TreeItem
- LeftSidebar kebab menu: "Hide from views" / "Show in views" with a
  mutation that invalidates folders, photos, stats, and tags caches
- Hidden folder rows swap the Folder icon for EyeOff and render the
  label italic/muted so the state is visible at a glance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:55:25 +02:00
root
07b1e5e02a feat: split celery workers, fix asyncpg-in-fork, add pipeline progress UI
Three overlapping fixes so the ingestion pipeline actually runs and the
user can see what it's doing:

Pipeline recovery
- app/database.py: use NullPool when MULITA_CELERY_WORKER=1 so each
  Celery task opens a fresh asyncpg connection on its own event loop.
  Fixes "another operation in progress" and "Future attached to a
  different loop" errors that were dropping ~every thumbnail +
  extract_metadata task on the floor.
- app/tasks/thumbs.py: initialize photo=None before the try and rollback
  on error so a transport failure in the initial SELECT doesn't raise
  UnboundLocalError in the except block and leak rows stuck in 'pending'.
- app/services/vision/bootstrap_models.py: on missing model files,
  invoke export_models automatically instead of just warning. First
  boot of a fresh install now self-heals.
- app/services/vision/export_models.py: shutil.move instead of
  Path.rename so the YOLO export survives the /app → /data/models
  cross-volume hop.
- requirements.txt: add ultralytics so export works in a stock image.

Worker topology
- docker-compose.yml: replace the single worker with worker-light
  (default/high/low queues, c=2, IO-bound) and worker-vision (vision
  queue, c=5, OMP_NUM_THREADS=1 to avoid oversubscription on 6 cores).
  Vision is pinned to ≤5 parallel inferences so ONNX doesn't each
  spawn an all-cores intra-op pool.
- .env / .env.example: CELERYD_CONCURRENCY replaced with
  CELERY_LIGHT_CONCURRENCY + CELERY_VISION_CONCURRENCY.
- Backfill queries in thumbs / scan / vision now ORDER BY taken_at
  DESC NULLS LAST so newest photos finish first — the library fills
  in top-down in the UI instead of arbitrary insertion order.

Settings visibility
- routers/library.py: new GET /maintenance/pipeline-stats returning
  done/total per stage (thumbnails, exif, gps, phash, embeddings,
  tags, ocr, faces, face clusters, duplicate groups). Worker-status
  now also reports the `vision` queue depth, which was missing.
- services/api.ts: PipelineStats / PipelineStage / ScanStatus types
  and the matching client call.
- components/dialogs/SettingsDialog.tsx:
  - new Pipeline Progress card with one progress bar per stage
  - inline scan banner (processed/total/current folder) inside the
    Library section while a scan is running
  - Tasks/min throughput computed by diffing worker processed counters
    between polls
  - Workers section calls out the vision queue and documents the
    CELERY_LIGHT/VISION_CONCURRENCY + docker compose up -d scale path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 10:06:45 +02:00
root
afe420c620 fix: use CPU-only PyTorch and run init_db before alembic migrations
PyTorch default install pulls ~7GB of CUDA libs, exceeding disk on small
VMs. Switching to CPU-only saves ~6GB. Also run create_all before alembic
so migrations find existing tables on a fresh Postgres.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:23:32 +02:00
532932057f style: mule ASCII placeholder in sidebar, bump muted text contrast
Replace Info icon with braille mule art in the right sidebar empty
state. Lighten text-muted (#a8997d → #c4b599) and text-faint
(#5e5448 → #7a6e5e) for better readability across the app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:13:54 +02:00
a4f64fad58 docs: update README for vision pipeline and Postgres stack
Reflect current state: Postgres+pgvector replaces SQLite, vision
pipeline (YOLO, CLIP, InsightFace, OCR) is shipped, card-grid browse
views for tags/colors/ratings/people, map view, duplicate detection,
and semantic search are all live. Remove completed items from future
features.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:50:28 +02:00
4bc6dc1dc8 feat: refactor grouping views into card-grid browse pattern
Replace Timeline-based grouped views (tags, colors, rated) with
dedicated card-grid components that drill into Timeline detail views
on click/Enter. Adds shared useCardGridNav hook for arrow-key
navigation across all four card grids (tags, colors, rated, people).

- TagsView, ColorsView, RatedView: card grid → inline Timeline detail
- PeopleView: migrated to same pattern (Timeline replaces custom grid)
- Tags endpoint: fall back to first associated photo for representative
- Filter store: add ratingMax for exact rating filtering in RatedView
- Timeline: remove tag/rating/color grouping; skip date headers when
  groupBy != 'date' so detail views render flat grids
- SettingsDialog: bump z-index above Leaflet map layers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:48:50 +02:00
fa9b21856f feat: replace face pipeline with InsightFace, add content classifier
Face detection/recognition:
- Replace YuNet + SFace with InsightFace buffalo_l (RetinaFace + ArcFace)
- 512-d ArcFace embeddings (was 128-d SFace), migration 0006 resizes column
- Remove YOLO person-bbox workaround — RetinaFace is accurate enough
- Detection threshold 0.65 cleanly separates real faces (0.72+) from
  false positives on dogs/paintings (0.56-0.61)

Content-type classification:
- CLIP zero-shot classifier using native PyTorch text encoder + ONNX
  image encoder for high-quality text-image similarity
- Categories: photograph, screenshot, document, receipt, meme, artwork
- Writes Tag(kind=content_type) per photo via photo_tags
- Margin-based confidence: top-1 vs top-2 score difference
- New ClassifierSettings in config (enabled, min_confidence)
- Wired into vision_fanout pipeline

Tested: 6 real faces from 4 photos (zero false positives), 11/13 photos
classified (8 photograph, 2 artwork, 1 meme).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:49:02 +02:00
f48e099bd2 fix: verify faces against YOLO person detections for precision
Cross-reference face detections with YOLO 'person' bounding boxes —
only keep faces that overlap >= 50% with a detected human body. This
eliminates false positives on dogs, paintings, and cartoons without
needing an aggressive score threshold.

Lower face detection threshold back to 0.6 since the person-overlap
check is now the primary precision filter.

Tested: 6 verified faces from 4 photos, zero false positives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:13:01 +02:00
40d570f2c2 fix: raise face detection threshold to 0.85, tighten clustering
Eliminates false positives (dog faces, painting faces) by requiring
score >= 0.85. Tighten cluster eps to 0.25 for better separation.
Tested: 4 real faces from 2 photos, no false positives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:09:43 +02:00
5f3fa5240e fix: search endpoint — correct column name and DISTINCT/ORDER BY
- Fix Photo.created_at → Photo.added_at (column doesn't exist)
- Fix Postgres DISTINCT + ORDER BY conflict by using a subquery for
  tag_id filtering instead of JOIN + DISTINCT on the outer query

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:55:55 +02:00
aba061dd43 fix: People view shows person's photos inline instead of navigating away
Clicking a person card now opens a detail sub-view within the People
section showing their photo grid. Back arrow returns to the card grid.
Photos are clickable to open the preview. Rename is available in both
the card grid and the detail header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:51:22 +02:00
229611b4c3 fix: face clusters write photo_tags, raise detection threshold to 0.7
- recluster_faces now writes photo_tags rows for each face cluster so
  the tag count and tag_ids filter work (previously count was always 0)
- Old cluster tags and photo_tags are cleaned up before re-clustering
- Raise face detection threshold from 0.4 to 0.7 to reduce false
  positives (was detecting dog faces as people)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:48:19 +02:00
db20cbb7d8 fix: face detection — use OpenCV FaceDetectorYN and full-res originals
- Rewrite faces.py to use cv2.FaceDetectorYN instead of raw ONNX
  (handles multi-scale anchor decoding and NMS internally)
- Load original photo files at up to 4000px for face detection instead
  of 240px thumbnails — faces were too small to detect at thumbnail res
- Falls back to thumbnail if original is unavailable

Tested: 33 faces extracted from 13 photos, clustered into 1 person.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:43:29 +02:00
17a69a271e feat: add People view with person cards and click-to-filter
Replace the tag-grouping people section with a dedicated PeopleView:
- Grid of face cluster cards showing representative photo thumbnail,
  person name, and photo count
- Click a card → navigates to all-photos filtered by that person's tag
- Inline rename via pencil icon on hover
- Empty state when no faces have been clustered yet

Wired into App.tsx as a section-level route alongside Map and Duplicates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:32:20 +02:00
7558aeb5e6 fix: sync DB sessions in Celery, letterbox YuNet, dedupe detections
- Rewrite all vision tasks to use sync psycopg2 sessions instead of
  asyncpg — fixes 'another operation in progress' and event loop errors
  when Celery forks workers sharing the async connection pool
- Letterbox-pad images to exactly 640x640 for YuNet face detector
  (was crashing on non-square thumbnails)
- Deduplicate object detections per label per photo — keep highest
  confidence only to avoid photo_tags PK violation on multiple
  detections of the same class
- Add all queues (-Q default,high,low,vision) to worker command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:28:17 +02:00
2a6661f779 fix: model weights setup — export scripts, ORT compat, bootstrap
- Add export_models.py for OpenCLIP ViT-B/32 and YOLOv8n ONNX export
- Fix ArgMax(13) ORT ARM64 incompatibility by passing eot_indices as a
  separate ONNX input (computed outside the graph in embed.py)
- Use legacy TorchScript exporter (dynamo=False) for IR version 9 compat
- Upgrade onnxruntime to 1.18.1
- Rewrite bootstrap_models.py with clear separation of auto-downloadable
  models (YuNet, SFace) vs manually-exported ones (OpenCLIP, YOLOv8n)
- Wire bootstrap into worker CMD (runs before Celery)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:41:25 +02:00
29177f0c1a feat: wire frontend to vision pipeline search and tags
- Add search API client (POST /photos/search) and useSearchQuery hook
  for hybrid FTS + semantic search with RRF ranking
- Extend Tag type with kind, source, representative_photo_id fields
- Add tags.merge() API method
- Update useTagsQuery to accept optional kind filter
- Add People section to sidebar (face clusters from GET /tags?kind=face_cluster)
- Sidebar Tags count now shows user tags only; People shows face clusters

The existing GET /photos?q= flow is preserved for browsing; the new
search hook activates when the search box has a non-empty query.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:19:47 +02:00
ad007e4cd4 feat: add face detection, recognition, and clustering
- Create face_embeddings table with pgvector Vector(128) + HNSW index
- Implement extract_faces task (YuNet detection + SFace recognition)
- Implement recluster_faces task (DBSCAN clustering → Tag(kind=face_cluster))
- Clusters are named "Person N" and get representative_photo_id
- cluster_id FK → tags.id, SET NULL on delete for merge/rename support

Migration 0005 creates the face_embeddings table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:13:41 +02:00
1ebc4bfe73 feat: add YOLO object detection writing to unified Tag model
Implement detect_objects Celery task:
- Runs YOLOv8n on 640px thumbnail via ONNX Runtime
- Creates Tag(kind=object) rows for each COCO class detected
- Writes photo_tags associations with confidence, bbox, and source
- Wipes previous detections per source model on re-run

No new tables/migrations — uses the unified Tag model from PR3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:11:26 +02:00
842a4fc864 feat: add OCR text extraction and Postgres full-text search
- Create ocr_text table for storing per-region OCR results
- Add tsvector search_vector column to photos with GIN index and
  auto-update trigger on filename/user_title/user_notes
- Implement ocr_photo Celery task using rapidocr-onnxruntime
- Add FTS leg to hybrid search: queries photos.search_vector and
  ocr_text via UNION, fused with semantic results via RRF (k=60)

Migration 0004 backfills search_vector for existing rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:10:14 +02:00
649437dc85 feat: add embeddings pipeline and semantic search endpoint
Wire the full embedding flow:
- Rewrite Embedding model to use pgvector Vector(512) with HNSW index
- Add embed_photo, vision_fanout, backfill_vision Celery tasks on
  dedicated `vision` queue
- Hook vision_fanout into generate_thumbnails completion
- Add POST /api/v1/photos/search with hybrid RRF ranking (semantic-only
  for now; FTS leg added in PR5)
- Stub ocr_photo, detect_objects, extract_faces tasks for later PRs

Migration 0003 drops/recreates the embeddings table (was never populated).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:07:32 +02:00
b1c2bdf7f0 feat: extend Tag model for unified ML tagging
Unify object detections, scene labels, and face clusters with user tags
via new columns on the existing Tag model:
- kind (user|object|scene|face_cluster), source, representative_photo_id
- photo_tags gains confidence, bbox (JSONB), source per-association
- Uniqueness moves from (name) to (name, kind) so ML labels coexist
  with user tags without collision

Add Alembic migration 0002 with defensive IF NOT EXISTS guards.

Update tags router: kind filter on GET, merge endpoint for combining
auto-detected clusters/objects, include kind/source in list response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:02:32 +02:00
9282a5c734 feat: add vision pipeline scaffolding with ONNX backend
Introduce the app/services/vision/ module with ABC interfaces, ONNX
Runtime backend, model registry, and per-task implementations:
- OpenCLIP ViT-B/32 embedder (image + text, 512-d)
- RapidOCR engine (PP-OCRv4 via ONNX, no PaddlePaddle)
- YOLOv8n object detector (raw ONNX, no ultralytics runtime)
- YuNet + SFace face processor (Apache 2.0, opencv_zoo, 128-d)
- DBSCAN face clustering helper

Add VisionSettings to config (mulita.yml + Pydantic), bootstrap_models.py
for first-boot weight downloads, models_data Docker volume, and ROCm
backend stub for future GPU acceleration.

No Celery tasks wired yet — models load but nothing invokes them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:00:06 +02:00
dea04ceed9 feat: migrate to Postgres + pgvector with Alembic scaffolding
Switch the default database from SQLite to Postgres + pgvector (via
pgvector/pgvector:pg16 Docker image) to support the upcoming vision
pipeline (embeddings, OCR, object detection, face clustering).

- Add `db` service to docker-compose.yml with healthcheck
- Wire `alembic upgrade head` into backend CMD before uvicorn
- Bootstrap empty 0001_baseline revision (schema still owned by create_all)
- Guard SQLite-only PRAGMAs and inline ALTERs behind _is_sqlite flag
- Run `CREATE EXTENSION IF NOT EXISTS vector` on Postgres init
- Add asyncpg, psycopg2-binary, pgvector to requirements
- Provide docker-compose.sqlite.yml escape hatch for legacy SQLite mode

Fresh DB + rescan assumed — no SQLite→Postgres data migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:46:20 +02:00
f01b5ed77e feat: snappier timeline — instant discard, progressive load, restore preview origin
- Discard yanks photos from the grid optimistically (cache strip + active
  cursor advance) instead of waiting for the mutation round-trip; wired
  through the X hotkey, RightSidebar bulk discard, LeftSidebar discard
  drop, and DiscardActionBar restore/delete.
- usePhotosQuery resolves on the first 500-photo page and streams the
  remaining pages into the cache in the background, so the first
  thumbnails paint immediately on large libraries.
- Closing preview restores the photo it was opened on (snapshot ref in
  PreviewView, written directly to the store) and Timeline scrolls that
  row back into view. Escape is handled on the dialog with
  stopPropagation so Timeline's window-level Esc handler doesn't wipe
  the restored selection.
- Preview overlay bumped to z-[1000] so it covers Leaflet map tiles,
  and the right sidebar no longer collapses during preview — both fix
  visible layout shifts on close.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:37:33 +02:00
b2ebf401bb chore: text format 2026-04-09 23:48:16 +02:00
57788f60c5 chore: move Tags section directly under Color label
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:48:02 +02:00
7cf546af7a feat: map view with GPS extraction fix
Adds a new Map sidebar entry that plots photos by their EXIF GPS
coordinates on a clustered Leaflet map. While wiring this up, the
metadata extractor was reading unprefixed GPS keys that never exist
in `exiftool -G -j` output AND assumed coordinates were already
floats — every photo silently lost its GPS. The new extract_gps
helper handles Composite/EXIF group prefixes and parses DMS strings,
and lat/lon are stored as first-class indexed columns so the map
can query them without parsing exif_json on every request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:44:29 +02:00
9c9f5bd899 chore: change text style 2026-04-09 20:43:33 +02:00
1c6383e5a0 feat: colored count badge on Colors sidebar entry
Adds a `colored` field to /library/stats counting non-discarded
photos with a color_label set, mirroring how `rated` is exposed.
The Colors sidebar entry now shows the live count and refreshes
through the existing LIBRARY_STATS_QUERY_KEY invalidations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:43:19 +02:00
c973ca0443 feat: colors view and color label thumbnail ornament
Adds a Colors entry under Views that buckets photos in canonical
R-O-Y-G-B-P order (plus an Uncolored tail), mirroring the rated/tags
grouping pattern. Surfaces the color label on each thumbnail as a
small swatch chip preceding the rating badge in the BL corner so
labels are visible everywhere, not just inside the new view. Also
types color_label on the Photo interface — the backend already
returned it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:41:06 +02:00
870e7dd3d3 fix: refresh sidebar counts after photo mutations
LIBRARY_STATS_QUERY_KEY was never invalidated by the bulk/single
mutation paths in RightSidebar and PhotoInfoPanel, so the All Photos
/ Rated / Discarded / Tags badges only updated on reload. Add it to
both shared invalidation helpers and the inline updateMutation, and
correct the stale docstring on useLibraryStatsQuery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:35:17 +02:00
30cbcb8cd1 feat: group rated view by star rating
Mirrors the tags view: rated section now buckets photos 5★→1★ with
an "Unrated" tail group, instead of a flat ratingMin=1 stream.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:30:44 +02:00
03b99a40b0 fix: lock left sidebar row height on hover
Switch tree and heap rows from py-0.5 to a fixed h-[24px] + leading-none
so the hover-only kebab buttons can't stretch the row vertically as the
pointer crosses them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:22:13 +02:00
b993a188d3 style: tint topbar "Built with hubris" footer to black/50
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:22:13 +02:00
367d1277b8 fix: stronger drop shadow on heap card thumbnails
Bump the ActiveHeapCard stack thumbnails to a layered drop shadow and
a darker ring so they lift cleanly off the softened desert backdrop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:11:33 +02:00
7d1149aabd feat: themed scrollbars and chiseled thumbnail chips
- Replace browser scrollbars with slim 6px overlay pills that tint to
  primary on hover (transparent track, Firefox + WebKit).
- Rework the thumbnail badge family: drop the mismatched white ring
  halo for a 1px dark frame + inset top highlight, and swap rounded-
  full pills for rounded-sm chips so the ornaments match the pixel-art
  aesthetic used elsewhere in the app.
- Soften the ActiveHeapCard desert backdrop so the fanned thumbnails
  read on top.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:09:49 +02:00
8f25c58b74 refactor: tighten sidebars with eyebrow section headers
Compact the left/right sidebars and the photo info panel: shrink panel
widths, drop header height, switch top-level tree groups and metadata
sections to small uppercase eyebrow labels, and tighten row padding,
icon sizes, and count badges throughout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:03:22 +02:00
12a616d4b5 fix: align search input height with filter pills
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:53:17 +02:00
2e7158a5bb feat: desert dusk theme with animated mule and pixel-art scenery
Replaces the cool-grey neutral theme with a warm desert dusk palette
pulled from a new pixel-art TopBar: a tiled, right-to-left scrolling
desert under a sky gradient, a 6-frame walking mule sprite where the
logo used to sit, and an ASCII block-character title on a black plate.
The active heap card now uses a cactus/dune scene as its stack
backdrop, and the "Built with hubris" mark moves into the TopBar's
bottom-right corner (AppFooter component removed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:49:13 +02:00
938c5dce68 refactor: unify thumbnail ornaments into one chip family
Every overlay on a thumbnail now composes the shared THUMB_BADGE_* classes
(one shape, one height, one ring, three semantic colour variants: primary
for user state, neutral for metadata, pick for auto-suggested best). RAW
badges, BEST pill, Keep-this button and the dimensions chip — previously
three different styles — join the family, is_duplicate moves to neutral
since it's file metadata not a user decision, and the selection check
shrinks to match the rest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:57:55 +02:00
6d8e92940a feat: auto-focus first photo on timeline mount
Arrow-key navigation required a click to establish an active photo
first. Now the grid auto-selects photos[0] on initial mount when no
active photo is set, so the user can land on the app and immediately
walk the grid with arrows. Guarded on viewMode === 'grid' and
!activePhotoId so we never clobber an existing selection or fight
with PreviewView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:47:16 +02:00
f4c51f6f92 fix: make keyboard hint pill legible over busy thumbnails
The bottom-center hint bar used bg-surface/40 + text-text-muted, which
left busy photos leaking through and washed the text out — users
couldn't actually read the shortcut labels when a bright thumbnail sat
directly behind the pill. Switched to a near-opaque bg-black/80 with
white-alpha text + white/15 key caps. Backdrop blur stays for the
subtle glass feel, but the contrast is now unconditional on the
photo underneath.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:44:04 +02:00
64b84e2069 feat: active heap card in left sidebar with subtle toasts
- New ActiveHeapCard pinned just below the Library header in the left
  sidebar. Renders null when no heap is active. When one is, shows the
  heap name + member count + a fan of the last 5 member thumbnails
  (newest front-and-center, older members rotated ±6°/±18px outward).
  Clicking the header navigates to the heap via the same
  navigateToSection pattern HeapsPanel uses.
- Stack animates with framer-motion (previously pinned in
  package.json but unused). New picks spring-slide into the front of
  the stack by subscribing to the ['heap-photo-ids', heapId] cache
  that the existing pick mutation already updates optimistically —
  no new event wiring. Unpicks run the exit transition and the
  remaining cards re-fan.
- Toasts are now subtle: glassy bg-surface/80 + backdrop-blur, thin
  2px left accent bar in the type color instead of a full tinted
  fill, smaller icons and text, tighter padding, truncation on
  overflow so they stay a single compact row. The colored alert
  block that was competing with the rest of the UI is gone.
- Card lives at the top of the sidebar specifically so the bottom-left
  toast stack can never occlude it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:35:06 +02:00
733c16bf82 feat: perceptual-hash duplicate detection + grouped picker view
The Duplicates section was useless: SHA-256-only detection only caught
byte-identical files, not the actual duplicates a real library
accumulates (re-encoded JPEGs, screenshots, resized exports), and the
view was a flat date-sorted list with no grouping or actions. This
replaces the whole flow.

Detection
- New phash + duplicate_group_id columns on Photo, added via an
  idempotent ALTER TABLE pass in init_db (the project has no Alembic).
- Thumbs worker computes a 64-bit pHash from the original-resolution
  decoded frame just before the destructive thumbnail loop. Falls back
  silently — phash is nice-to-have, not a blocker for thumbnails.
- backfill_phashes Celery task fills in phashes for photos that
  predated the column, reading the existing thumb_large rather than
  re-decoding the original.
- regroup_duplicates service runs union-find over Hamming distance
  (threshold 6), persists duplicate_group_id, and maintains is_duplicate
  as derived state so existing badges/counts keep working. Chained
  after scan_all_source_roots with a 60s countdown.

API
- GET /library/duplicates/groups returns all groups with members,
  bucketed in Python from one query. Each group has a reason ("exact"
  iff every member shares a SHA-256, "similar" otherwise).
- POST /library/maintenance/{regroup-duplicates,backfill-phashes}.

Frontend
- New DuplicatesView (sectioned grid, one section per cluster) replaces
  the timeline when the user is in the duplicates section. Each section
  shows a "Keep best, discard N" button that picks the highest-pixel
  copy and reuses the existing undoable bulk-discard so Cmd+Z works.
- Manual best override: hover any non-best thumbnail and click "Keep
  this" (Crown icon, top-right) to override the auto-pick. The header
  annotates "(manual)" so it's obvious which copy will be kept.
- Keyboard nav within the duplicates view walks the flat member list,
  with ↑/↓ jumping by the measured column count and scrollIntoView on
  every move. Timeline's keyboard handler now early-returns in the
  duplicates section so the two don't fight.
- BEST pill / Keep-this button live at top-right with a ring outline so
  they don't collide visually with the cyan selection ring around a
  selected cell. Dimensions chip moved to bottom-left to free both
  right corners for the keep affordances.
- New "Duplicates" section in SettingsDialog: shows group/member counts
  and exposes both backfill + re-detect actions, sharing a query cache
  with DuplicatesView via DUPLICATE_GROUPS_QUERY_KEY.
- PhotoInfoPanel "Basic Info" section now shows the photo's full file
  path in monospace below the size/dimensions/date grid.
- New imagehash==4.3.1 dep in requirements.txt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:19:08 +02:00