Commit Graph

5 Commits

Author SHA1 Message Date
204d2bf2a8 feat: simplify folder setup — single mount, auto bootstrap, browser dialog
Cleans up the maze of overlapping ways folders entered the app, plus
removes the dead trash plumbing left over from the soft-discard
refactor.

Setup model (now)
- ONE env var: PHOTO_DIRS in .env, set to the host path of your
  library. Compose mounts that at /photos. That's the entire setup.
- On first boot, the backend auto-creates a SourceRoot row named
  "Library" pointing at /photos so the user sees their photos
  immediately without configuring anything.
- Source roots and discard live in the database; mulita.yml only
  carries operational settings (thumbnails, scanner, performance).
- The "Add Source Folder" dialog is now a directory browser
  restricted server-side to /photos and any existing source root —
  the user clicks through actual mounted directories instead of
  typing container paths they can't possibly know.

Backend
- New services/scanner.bootstrap_default_source_root(): if no
  SourceRoot rows exist and /photos is mounted, create one. Wired
  into the lifespan handler before cleanup + initial scan.
- New GET /library/browse?path= returning the immediate child
  directories of `path`, validated to live under one of the allowed
  roots (default mount + every active SourceRoot). Hidden entries
  are filtered. Children are tagged with is_existing_root so the UI
  can show an "Added" badge. Returns parent path for up-nav, or
  null when at the top of the allowed scope.
- scan_all_source_roots now reads from the DB instead of the YAML
  config so DB-managed source roots are honoured by initial scan.
- Dropped the placeholder source_roots block from mulita.yml — the
  paths /photos/main and /photos/iphone never existed and just
  produced startup warnings.
- Dropped TrashSettings, settings.trash, settings.source_roots,
  and the SourceRoot pydantic model from config.py. Soft discard
  has owned this for a while; it was dead code.

Compose
- Single ${PHOTO_DIRS:-./photos}:/photos:rw mount in both backend
  and worker.
- Removed the hardcoded ~/Pictures:/host/Pictures:rw mount — the
  PHOTO_DIRS variable is the single source of truth now.
- Removed the trash_data named volume + mounts (no consumers).
- backend/Dockerfile no longer creates /data/trash; it now creates
  /data/proxies (which the proxy endpoint actually uses).

Frontend
- AddSourceFolderDialog rewritten as a directory tree picker:
  loads /library/browse on open, lets the user navigate up via a
  ChevronUp button or down by clicking subfolders, shows the
  current path inline, and adds whatever directory is currently
  shown. Existing source roots are tagged "Added" so the user
  knows what's already registered. Errors from the backend (e.g.
  trying to navigate outside the allowed scope) surface inline.
- New library.browse() helper + BrowseChild / BrowseResponse types
  in services/api.ts.

Docs
- README Quick Start rewritten around the single PHOTO_DIRS env
  var, with macOS/Linux/Windows examples.
- New "How mounted folders and source folders relate" section that
  spells out the two-layer model (mount = visibility, source root
  = scanning) so the most common confusion is addressed up front.
- Added a "Read-only libraries" subsection that lists exactly which
  endpoints fail under :ro.
- "Configuration" section reframed: source roots are managed by the
  UI/API now, mulita.yml is operational settings only.
- .env file now has examples for the common host paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:27:21 +02:00
a0c41e38d3 revert: drop By Date sidebar node and supporting code
Tried it, didn't add value beyond what the date-grouped timeline
already gives. The grouped, sticky-headered timeline (which kicks
in by default whenever sortBy is taken_at) is the better
affordance for date navigation — duplicating that as a sidebar
drilldown was just clutter.

Removes the full stack:
- LeftSidebar: by-date tree node, byDateChildren computation,
  date-year-/date-month- handlers in applyLibraryNode, isItemActive
  branches that matched a date-range filter, the now-unused
  setDateFrom/setDateTo/filterDateFrom/filterDateTo selectors, and
  the Calendar icon import.
- frontend/src/hooks/useDateBucketsQuery.ts deleted entirely.
- api.ts: library.dateBuckets helper and DateBucketYear/Month types.
- backend/app/routers/library.py: GET /library/date_buckets endpoint
  and its strftime aggregation query.

The dateFrom/dateTo filter state stays in filterStore — the
FilterBar still uses it for the "Date" range inputs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:19:59 +02:00
7d33e1688a feat: By Date sidebar — year/month drilldown navigation
The "By Date" library node was decorative. Now it's a real
hierarchical navigator: expand to see year buckets (with photo
counts), expand a year to see its months, click any year or
month to filter the timeline to that date range.

Backend
- New GET /library/date_buckets aggregates non-discarded photos
  by year+month from Photo.taken_at via SQLite strftime, returning
  [{ year, count, months: [{ month, count }] }] sorted newest-
  first. NULL taken_at rows are excluded.

Frontend
- New library.dateBuckets() helper + DateBucketYear / Month types.
- New hooks/useDateBucketsQuery.ts with a 60s staleTime.
- LeftSidebar builds the By Date subtree dynamically from the
  query: each year is a tree node with month children. Year nodes
  use the Calendar icon, months render as their full English name.
- applyLibraryNode handles two new id prefixes:
  'date-year-{year}'  → setDateFrom YYYY-01-01, setDateTo YYYY-12-31
  'date-month-{Y}-{M}'→ setDateFrom YYYY-MM-01, setDateTo YYYY-MM-LL
  where LL is the last day of the month (computed via Date trick
  new Date(year, month, 0).getDate() — uses month-day=0 to roll
  back into the previous month's last day).
- isItemActive recognises when the current dateFrom/dateTo matches
  a year or month node so the sidebar selection highlight stays
  in sync with the filter store (also when filters are set
  externally via the filter bar or URL hydration).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 00:14:57 +02:00
78e12e8309 feat: timeline and folder import 2026-04-07 00:42:22 +02:00
46a0d7aba8 feat: structure 2026-04-06 23:30:19 +02:00