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

+11 -4
View File
@@ -32,7 +32,9 @@ export function openDb(dbPath) {
width INTEGER,
height INTEGER,
filesize INTEGER,
starred INTEGER NOT NULL DEFAULT 0
starred INTEGER NOT NULL DEFAULT 0,
place_city TEXT,
place_country TEXT
);
CREATE INDEX IF NOT EXISTS idx_photos_date_taken ON photos(date_taken);
@@ -96,7 +98,10 @@ export function openDb(dbPath) {
// Column migrations — run before any indexes that depend on the new column
try { db.exec('ALTER TABLE photos ADD COLUMN starred INTEGER NOT NULL DEFAULT 0'); } catch {}
db.exec('CREATE INDEX IF NOT EXISTS idx_photos_starred ON photos(starred)');
try { db.exec('ALTER TABLE photos ADD COLUMN place_city TEXT'); } catch {}
try { db.exec('ALTER TABLE photos ADD COLUMN place_country TEXT'); } catch {}
db.exec('CREATE INDEX IF NOT EXISTS idx_photos_starred ON photos(starred)');
db.exec('CREATE INDEX IF NOT EXISTS idx_photos_place_city ON photos(place_city)');
// Seed default settings
const seedSetting = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
@@ -106,10 +111,12 @@ export function openDb(dbPath) {
insert: db.prepare(`
INSERT OR IGNORE INTO photos
(filepath, thumb_path, preview_path, date_taken, date_imported,
camera_model, gps_lat, gps_lon, phash, width, height, filesize)
camera_model, gps_lat, gps_lon, phash, width, height, filesize,
place_city, place_country)
VALUES
(@filepath, @thumb_path, @preview_path, @date_taken, @date_imported,
@camera_model, @gps_lat, @gps_lon, @phash, @width, @height, @filesize)
@camera_model, @gps_lat, @gps_lon, @phash, @width, @height, @filesize,
@place_city, @place_country)
`),
exists: db.prepare('SELECT 1 FROM photos WHERE filepath = ? LIMIT 1'),
remove: db.prepare('DELETE FROM photos WHERE filepath = ?'),