feat: places (reverse geocoding) + folder browser

Places:
- worker/geocode.js: Nominatim reverse geocoder, 1 req/sec rate limit,
  in-memory cache keyed to 3-decimal-place coordinates
- Ingest calls reverseGeocode() for any photo with GPS data; stores
  place_city and place_country on the photo row
- DB migrations add place_city / place_country columns + index on place_city
- Stats API returns cities grouped by count
- buildFilter() supports ?city= parameter
- FilterSidebar shows a Places section with city list + country sublabel

Folder browser:
- GET /api/fs/browse?path=… lists subdirectories (path-normalized to prevent traversal)
- Settings panel: 📁 button opens inline directory browser below the add
  input — navigate with up arrow / directory clicks, Select fills the input

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-18 08:27:41 -04:00
1 parent 64f7906b22
commit 3429ec1d80
8 files changed
+264 -25

No files matched your search

+2 -2
View File
@@ -10,7 +10,7 @@ import SettingsPanel from './components/SettingsPanel.jsx';
import { lazy, Suspense } from 'react';
const MapView = lazy(() => import('./components/MapView.jsx'));
const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '' };
const EMPTY_FILTERS = { q: '', from: '', to: '', tagId: '', collectionId: '', starred: '', city: '' };
const SIDEBAR_W = 210;
export default function App() {
@@ -101,7 +101,7 @@ export default function App() {
loadStats();
}
const hasFilter = filters.q || filters.from || filters.to || filters.tagId || filters.collectionId || filters.starred;
const hasFilter = filters.q || filters.from || filters.to || filters.tagId || filters.collectionId || filters.starred || filters.city;
return (
<>