first commit
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Project-local AI memory (`.claude/memory/`)
|
||||
|
||||
This folder is the **project-local memory for AI assistants**, committed with the code (not in any
|
||||
machine's global `~/.claude`). Any developer or Claude/AI account that opens this package should read
|
||||
these files for full context. Keep them updated as work proceeds.
|
||||
|
||||
- [status.md](status.md) — current build status + session log (what's done, what's next).
|
||||
- [requirements.md](requirements.md) — the COMPLETE requirements backlog (every request, tracked).
|
||||
- Human-facing docs live in [`/docs`](../../docs/) (ROADMAP / SETUP / ARCHITECTURE) and [`/CLAUDE.md`](../../CLAUDE.md).
|
||||
|
||||
## Hard rules (do not violate)
|
||||
- **Never `git commit` / `git push`** — the user does that. Never commit secrets.
|
||||
- API keys live only in `apps/web/.env.local` (gitignored), read **server-side** only.
|
||||
- Every feature is **optional via `features` flags**. End goal: **backend (Supabase), no localStorage reliance**
|
||||
(localStorage is the dev fallback only until Supabase creds are provided).
|
||||
- Prefer **free** tech; if no free option, build our own. If a cloud key (e.g. Gemini) is missing/invalid,
|
||||
fall back to a **local model** (e.g. `@imgly/background-removal` for remove-bg).
|
||||
- UI must be **meaningful for a WEB gallery** (not iOS) — e.g. no "App Store"; "Albums" not "Projects".
|
||||
@@ -0,0 +1,46 @@
|
||||
# Requirements backlog (complete)
|
||||
|
||||
Status: ✅ done · 🚧 in progress · ⬜ planned · 🔑 needs key/creds. Cross-ref `/docs/ROADMAP.md`.
|
||||
|
||||
## Done (Phases 1–3)
|
||||
- ✅ macOS-Photos UI: sidebar, toolbar, Library (Years/Months/All), Collections, Map (map/satellite/grid),
|
||||
People, Recently Deleted (locked), lightbox, light/dark/**semi-dark**, responsive to 280px, a11y, nonce CSP.
|
||||
- ✅ Albums (CRUD/nested/smart), source auto-classify, recycle bin, duplicates, multi-select, import (file+drag),
|
||||
export, **EXIF** (GPS/date/camera) on import.
|
||||
- ✅ Object detection (TF.js, free) → tags/search/find-similar/**Objects browser**/object focus.
|
||||
- ✅ Annotations (rect/oval/line/arrow/**double-arrow measurement**/text/freehand + colors) in editor + lightbox.
|
||||
- ✅ Info panel (full metadata + **mini-map**). Themes menu + `accentColor`/`borderRadius` props.
|
||||
- ✅ Gemini generative editor (Remove BG/Restore/Colorize/Replace Sky/prompt) — 🔑 needs billed key (free=429).
|
||||
|
||||
## Now / next (in user's priority order)
|
||||
1. 🚧 **Custom camera** (#1): getUserMedia preview; capture photo; record video; switch front/back; grid;
|
||||
**live annotation + measurement**; geolocation on capture (permission); auto object-detect after capture.
|
||||
2. ⬜ **Supabase backend + storage** (#last, 🔑): Postgres data + Supabase Storage. **No localStorage reliance** —
|
||||
backend is the source of truth. Ask user for creds (URL, anon key, service-role key, bucket) with steps.
|
||||
3. ⬜ Face clustering → People; OCR (tesseract.js) → document search; semantic "show me beach photos"
|
||||
(transformers.js / Hugging Face CLIP) — all free, in-browser.
|
||||
4. ⬜ Video editor + annotations; macOS-parity player polish.
|
||||
|
||||
## New requests (this session — must add)
|
||||
- ⬜ **Editor: straighten / tilt** (angle slider) — rotate by arbitrary degrees.
|
||||
- ⬜ **Crop: free-form AND fixed-ratio** — when a ratio (e.g. 1:1 square) is selected, dragging/stretching the
|
||||
crop box keeps that aspect ratio; also free crop.
|
||||
- ⬜ **Import sync of existing metadata**: if an imported/mobile image already has tags (IPTC/XMP keywords) +
|
||||
location + details, read and apply them (exifr supports XMP/IPTC). Keep object detection + object albums.
|
||||
- 🚧 **Web-meaningful UI** (deep pass): "All Projects" → **"All Albums"** (this is a web gallery, not iOS);
|
||||
remove "Open App Store" (meaningless on web); review sidebar tabs, **corner radius**, touch-ups so everything
|
||||
is meaningful for a web gallery and matches macOS more closely.
|
||||
- ⬜ **Search dropdown defaults**: show quick filters **Recently Viewed**, **Recently Edited** (+ Recently Shared)
|
||||
when the search field is focused.
|
||||
- ⬜ **Info → map click opens full Map** centered on that photo's location.
|
||||
- ⬜ **Map parity w/ macOS**: locations + images + tags; latest image shown for a tag; **click a tag → open
|
||||
that tag's images grouped by date** (date-wise). Tag = album-like view, date-grouped.
|
||||
- ⬜ **Local-model fallback**: if Gemini key missing/invalid, use a local model. Concretely wire
|
||||
`@imgly/background-removal` (free, in-browser) for Remove Background so it always works without a key.
|
||||
- ⬜ **Everything free**; build our own if no free option.
|
||||
- ✅ **Memory in project `.claude/` folder** (this folder) — done.
|
||||
- ⬜ More **advanced features** (open-ended; user will add more later).
|
||||
|
||||
## Notes
|
||||
- getUserMedia needs HTTPS or localhost (dev OK). Permissions-Policy already allows camera/mic/geolocation=self.
|
||||
- When Supabase is wired, switch the demo adapter from localStorage → Supabase and drop localStorage reliance.
|
||||
@@ -0,0 +1,309 @@
|
||||
# Build status & session log
|
||||
|
||||
## Run
|
||||
`pnpm install && pnpm dev` → http://localhost:3000 (landing → Open Gallery → /gallery). `pnpm build` to ship.
|
||||
|
||||
## Current state (2026-06-29)
|
||||
Phases 1–3 complete & verified (see [requirements.md](requirements.md)). `pnpm build` passes; /gallery ~170 KB
|
||||
(TF.js + exifr lazy). Zero console errors. Default persistence = localStorage adapter (until Supabase wired).
|
||||
|
||||
## Session log
|
||||
- S1: monorepo + SDK + demo; full macOS Photos UI; albums/smart-albums/recycle/import/editor/lightbox; light+dark;
|
||||
responsive; nonce CSP; 19-finding review fixed.
|
||||
- S2: free in-browser object detection (TF.js COCO-SSD) + Objects browser; Gemini generative editor (server route,
|
||||
key server-side; 429 on free tier); fixed ORB video (local /sample-clip.mp4).
|
||||
- S3: map popups; annotations incl. measurement double-arrow; EXIF; Info panel + mini-map; semi-dark + theming props;
|
||||
docs/ + CLAUDE.md.
|
||||
- S4: `.claude/memory/` created; web-meaningful UI (All Projects→**All Albums**, removed App Store; AlbumsOverview);
|
||||
**custom camera (#1) DONE** (`components/Camera.tsx`: getUserMedia preview, photo capture, video record,
|
||||
front/back switch, rule-of-thirds grid, live annotation+measurement, geolocation on capture, auto object-detect;
|
||||
store `cameraOpen`/openCamera/closeCamera; toolbar 📷 button). Verified modal UI (no camera device in test env;
|
||||
works with a real webcam). `pnpm build` passes (/gallery 172KB).
|
||||
STILL OPEN: editor straighten/ratio-crop, search defaults (Recently Viewed/Edited), import IPTC/XMP tags,
|
||||
local remove-bg fallback (@imgly), info-map→Map, tag→date-wise, face clustering, OCR, semantic search, video annotations.
|
||||
- S5: **#2 Supabase backend DONE** — `apps/web/src/lib/adapters/supabaseAdapter.ts` (Postgres JSONB tables +
|
||||
Storage blobs via putBlob); store gained `importFiles`/`uploadBlob` (uploads on import + camera capture);
|
||||
TopToolbar/LibraryView/Camera route through them; GalleryClient uses Supabase when `NEXT_PUBLIC_SUPABASE_*` set
|
||||
(else localStorage). CSP (middleware) now allows `https://*.supabase.co` (connect+img+media). Bucket `media` made
|
||||
public. Creds in `apps/web/.env.local` (gitignored). Adapter degrades gracefully if tables missing (probe→warn→seed).
|
||||
**USER MUST run `docs/supabase-setup.sql` once (SQL Editor) + reload** to activate persistence. `pnpm build` passes
|
||||
(/gallery 234KB — supabase-js bundled; TODO lazy-load to trim). Verified app boots clean on Supabase adapter (seed
|
||||
fallback) — only the expected gallery_media 404 probe until SQL is run.
|
||||
NOTE: service_role key is in .env.local but the client adapter uses the anon key; service key only for admin/setup.
|
||||
- S6: VERIFIED Supabase end-to-end (user ran SQL): app loads 45 rows from Postgres, 0 console errors; a camera
|
||||
capture persisted + round-trips. FIXES: camera annotations moved to the REVIEW stage (annotate the captured still
|
||||
→ saved as edits.annotations) + Undo/Clear; geolocation prefetched on camera open (10s, high-accuracy) + cached;
|
||||
device info attached to capture exif (Make 'Web Camera', Model=track label, Resolution). Auto-albums: addMedia now
|
||||
auto-creates+fills "Camera" (source camera) and "Screenshots" (source screenshot) user albums. Recycle: init purges
|
||||
items trashed > 30 days (deletePermanently → syncs to Supabase); restore/permanent-delete already sync via save.
|
||||
NEW FEATURES: editor **Straighten** slider (EditState.straighten + editTransformCss cover-scale); **info mini-map
|
||||
click → focusMap → opens full Map centered** (store mapFocus); **tag chip click → setTagFocus → library filtered by
|
||||
tag, date-grouped** (store tagFocus, libraryScale='days'); object/tag chips close overlays. VERIFIED tag→date-wise live.
|
||||
`pnpm build` passes.
|
||||
DEFERRED (next): interactive fixed-ratio/free crop (bake on save), search dropdown defaults (Recently Viewed/Edited),
|
||||
import IPTC/XMP keywords, local remove-bg fallback (@imgly), face clustering/OCR/semantic search, video annotations,
|
||||
lazy-load supabase-js (trim /gallery bundle), storage blob deletion on permanent-delete, reverse-geocode place names,
|
||||
optional pg_cron for server-side 30-day purge.
|
||||
- S7: **Interactive crop DONE & verified** — `components/editor/CropBox.tsx` (drag/resize, 4 corner handles, masks,
|
||||
thirds grid, aspect lock so square stays square), ratio presets (Free/Square/4:3/3:4/16:9/9:16/Original) in the
|
||||
Crop tab; `lib/bake.ts` flattens crop+flip+filter+vignette+rotation/straighten to a JPEG on Done and re-maps
|
||||
annotations; `PhotoEditor.save()` bakes when `hasGeometry(edits)` and uploads via `uploadBlob` (Supabase Storage).
|
||||
FIXED Supabase storage 400: upsert/overwrite violated RLS → adapter now uploads to a UNIQUE path per call
|
||||
(`${id}-${rand}.ext`, plain INSERT). VERIFIED: cropped a portrait screenshot to 16:9, baked + uploaded to bucket
|
||||
(object `eMdEj_Ow32-…jpg`), displays cropped; 0 console errors; `pnpm build` passes.
|
||||
STILL OPEN (next, in order): search dropdown defaults (Recently Viewed/Edited), import IPTC/XMP keywords,
|
||||
local remove-bg fallback (@imgly), face clustering / OCR / semantic search, video annotations, lazy-load supabase-js.
|
||||
- S8: **search dropdown defaults DONE** (store recentlyViewed[] + searchPreset 'viewed|edited|added'; openLightbox
|
||||
records views; setSearchPreset; selectors handle presets; TopToolbar Recents dropdown on focus). FIX: 'edited'
|
||||
preset keys off `editedAt||edits` (baked crops clear `edits` but set editedAt) — VERIFIED shows both crops + selfie.
|
||||
**IPTC/XMP import DONE** (readExif parses iptc+xmp → Keywords/dc:subject → tags). **Lazy-load supabase-js DONE**
|
||||
(GalleryClient dynamic-imports the adapter; /gallery 234KB→176KB). All typecheck + `pnpm build` pass; verified live.
|
||||
STILL OPEN (next): local remove-bg fallback (@imgly; needs wasm-unsafe-eval CSP + connect-src for model host),
|
||||
face clustering → People, OCR → documents, semantic search (transformers.js/HF), video annotations.
|
||||
- S9: **Local Remove Background DONE & verified** — `@imgly/background-removal` (lazy import) in
|
||||
`createDemoAIProvider.generativeEdit`: op 'remove-background' runs the in-browser WASM model (no Gemini);
|
||||
other ops still hit /api/ai/edit. CSP: added `'wasm-unsafe-eval'` to script-src + `blob: data:` and
|
||||
`https://staticimgly.com` to connect-src (the ML worker fetches model/wasm from blob: URLs). VERIFIED:
|
||||
removed background from the user's selfie capture → clean transparent PNG, 0 errors, saved+uploaded to Supabase.
|
||||
`pnpm build` passes (/gallery 177KB; @imgly lazy). protobufjs build script declined in pnpm-workspace.yaml.
|
||||
STILL OPEN (next, model-heavy): face clustering → People (face-api.js), OCR → Documents (tesseract.js),
|
||||
semantic search (transformers.js/CLIP), video annotations. Each needs a model CDN host in connect-src.
|
||||
- S10: **Face clustering → People DONE & verified** — `apps/web/src/lib/ai/faceProvider.ts` (`@vladmandic/face-api`,
|
||||
lazy import; tinyFaceDetector+faceLandmark68+faceRecognition; models from `cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.15/model`,
|
||||
added to connect-src). Wired as `detectFaces` in createDemoAIProvider. SDK: `lib/cluster.ts` (greedy euclidean
|
||||
clustering, threshold 0.55, biggest-face-first); store `rebuildPeople()` (reuses existing person id/name by
|
||||
media overlap so names survive re-clustering) + `personFocus`/`setPersonFocus`; selectors filter by personFocus;
|
||||
AIAnalyzer now runs detectObjects+detectFaces (only what's missing per item) and calls rebuildPeople() after the
|
||||
pass (and once on load if faces exist but People empty); PeopleView click → setPersonFocus (filters library +
|
||||
shows count); LibraryView banner shows "Person: …". BUG FIXED: `hooks/useViewMedia.ts` omitted personFocus from
|
||||
the state passed to mediaForView → filter was a no-op; added it (+dep). VERIFIED: face-api detected the selfie's
|
||||
face (1 face, 128-D emb), People shows 1 person w/ circular cover + "1 photo", click → grid filtered to that 1
|
||||
photo. Persisted to Supabase (gallery_people + media.personIds). `pnpm build` passes (/gallery 178KB; face-api lazy,
|
||||
benign "Critical dependency: require" webpack warning only). Picsum seed photos / screenshots have no detectable
|
||||
frontal faces → only the real selfie clusters, as expected.
|
||||
STILL OPEN (next): OCR → Documents (tesseract.js), semantic search (transformers.js/CLIP), video annotations.
|
||||
- S11: **OCR → Documents + searchable text DONE & verified** (planned via an `ultracode` Workflow that fanned out
|
||||
5 parallel readers to map integration points, then an Opus synthesizer). `apps/web/src/lib/ai/ocrProvider.ts`
|
||||
(NEW, tesseract.js@5.1.1, lazy import, single memoized worker; worker/core/lang paths ALL pinned to
|
||||
cdn.jsdelivr.net so nothing falls back to the non-allow-listed tessdata.projectnaptha.com; `meaningfulText()`
|
||||
confidence filter: per-word conf>=70 + >=4 words, else mean-conf>=72 + >=6 word-shaped tokens → '' for photos).
|
||||
Wired as `ocr` in createDemoAIProvider. SDK: `types.ts` MediaItem.ocrText + SmartRule 'hasText' + ViewId
|
||||
`sys:${string}`; `lib/text.ts` (NEW) sanitizeOcrText (control-char strip via RegExp-from-string, ws-collapse,
|
||||
10k cap); `smartAlbums.ts` hasText eval + `sys:documents` album; `selectors.ts` searchMedia haystack += ocrText;
|
||||
`AIAnalyzer.tsx` 3rd OCR leg (own `ocrText===undefined` gate, inner .catch so a slow/failed OCR can't abort
|
||||
objects+faces, patches even '' so it isn't reprocessed); Sidebar + CollectionsView 'Documents' + 'document' icon.
|
||||
CSP: cdn.jsdelivr.net added to script-src (connect-src already had it). CAUGHT VIA GROUND-TRUTH: first pass put
|
||||
ALL 41 Picsum photos in Documents because tesseract hallucinates low-conf gibberish (meanConf 21-29) on
|
||||
textureless photos → added the confidence filter; re-verified Picsum→'' , imported a text invoice → 213 chars of
|
||||
accurate text, Documents shows exactly it, search "oceanfront villa" (words only inside the image) finds it.
|
||||
Persisted to Supabase. `pnpm build` passes (/gallery 179KB; tesseract lazy). NOTE: ~189 benign console WARNINGS
|
||||
("kernel … already registered for backend webgl") appear when face-api's bundled tfjs + coco-ssd's tfjs coexist
|
||||
(triggered by analyzing a newly-imported image) — warnings only, 0 errors. A test invoice now lives in the demo
|
||||
library as a live Documents example (uploaded to Supabase; user can delete).
|
||||
ADVERSARIAL REVIEW (ultracode Workflow, 4 dims × find→refute, 15 agents): correctness + security CLEAN (0);
|
||||
6 false alarms correctly refuted (worker leak, rebuildPeople N+1 writes, sequential model load, CSP-nonce-for-
|
||||
blob-workers, etc.). FIXED 3 confirmed: (1) exported pure utils clusterFaces/FaceCluster/sanitizeOcrText/
|
||||
OCR_TEXT_MAX_CHARS from index.ts (SDK-pattern consistency); (2) pinned tesseract.js EXACT 5.1.1 in package.json
|
||||
(was ^5.1.1) so the hardcoded worker CDN version can't drift; (3) sanitizeOcrText truncates on a word boundary
|
||||
at the 10k cap (was mid-word → search false-negative). DOCUMENTED (not fixed — minor/pre-existing, not regressions):
|
||||
(a) OCR backfill gate `ocrText===undefined` won't re-run if a host disables then re-enables OCR (same pattern as
|
||||
faces; fine while OCR stays on); (b) persist() debounce means a mid-analysis crash loses recomputable derived
|
||||
metadata (pre-existing, affects all analysis; source media safe, re-derived next load).
|
||||
STILL OPEN (next): semantic search (transformers.js/CLIP), video annotations.
|
||||
- S12: **BIG new backlog from user (13 items)** — kicked off. WAVE 1 DONE & verified:
|
||||
(1) Selection is now a TOGGLE — plain click selects, clicking the sole selected tile deselects
|
||||
(PhotoTile.onClick); (2) the "..." More menu shows **Select All** then flips to **Unselect All** once all
|
||||
current-view items are selected (TopToolbar.openMoreMenu; removed the stray clearSelection-on-open); (3) editor
|
||||
now has **Save** (overwrite) + **Save as Copy** (uploads under a fresh id via new store action
|
||||
`duplicateWithEdits`, then opens the album picker) + **Cancel** with an "unsaved changes" confirm (isDirty check;
|
||||
Escape routes through it too via cancelRef); (4) **Copy to Album** (was "Add to Album") + **Move to Album**
|
||||
(new `moveToAlbumPicker`, album-view only) + Remove from Album. VERIFIED in browser: toggle true→false,
|
||||
Select All→46 selected→"Unselect All", editor bar = Cancel/Revert/Save as Copy/Save, context menu = Copy to Album.
|
||||
`pnpm build` passes (/gallery 180KB).
|
||||
PLANNING (ultracode background Workflow, 7 architects): file-level plans for the remaining big features saved at
|
||||
tasks/wck4qn659.output (sharing+links, password-lock, map-pins, grid-mosaic, glass-design, semantic-search,
|
||||
video-annotations) — use as the blueprint for the next waves.
|
||||
REMAINING WAVES (in this order): People rename + pet detection; Objects browser verify/expand; Map thumbnail pins
|
||||
+ click->location date-wise + screenshot pins; Map "Grid" mosaic (hero + verticals + auto-slide); Sharing/Shared
|
||||
Albums/Activity + download links; Recently Deleted password lock + Settings; macOS glass/dark/radius design polish;
|
||||
semantic search (CLIP); video annotations; then README + memory.
|
||||
Design ref captured from user's macOS screenshots: glass dropdowns/sidebar (backdrop-blur + translucent dark
|
||||
fill + hairline border + caret), thumbnail map pins w/ count badge, irregular mosaic Grid.
|
||||
- S13: **WAVE 2 DONE & verified** — People rename + pet detection + Objects browser.
|
||||
PETS: `constants.PET_LABELS` (dog/cat/bird/horse/rabbit); `store.rebuildPeople` now also builds pet groups from
|
||||
objectLabels with STABLE ids `pet:<label>` (name preserved across rebuilds), appended to people[], denormalized
|
||||
into media.personIds; AIAnalyzer triggers rebuild for objects too + on-load if pets present but ungrouped
|
||||
(imports PET_LABELS). RENAME: `store.renamePerson(id,name)`; PeopleView tile restructured (div, not nested
|
||||
buttons) — avatar button = view photos (personFocus), name button = rename via generalized `promptAlbumName`
|
||||
(now takes opts.placeholder/confirmLabel; NamePrompt placeholder prop). OBJECTS: already in CollectionsView
|
||||
(objectLabelCounts → setObjectFocus); verified. VERIFIED in browser: People & Pets shows human ("+ Add Name")
|
||||
+ Birds + Horses; renamed human → "Davy" (persisted to gallery_people); Objects lists all 14 detected types,
|
||||
click "boat" → 2 photos + "Object: boat" banner. `pnpm build` passes (/gallery 180KB).
|
||||
NEXT: Wave 3 = Map thumbnail pins (count badge, click->location date-wise, screenshots w/ GPS pinned) + Map
|
||||
"Grid" mosaic. Then sharing/activity+links, password-lock, glass design, semantic search, video annotations,
|
||||
README. Blueprints for those in docs/feature-plans.json.
|
||||
- S14: **WAVE 3 DONE & verified** — Map redesign.
|
||||
PINS: `selectors.clusterByLocation(items, precision)` (round coords; precision rises with zoom) + `LocationCluster`.
|
||||
MapView now renders Leaflet `divIcon` THUMBNAIL pins (`.apg-pin` img + count badge + tail; CSS in sdk.css) —
|
||||
static html only, img.src + count set via DOM after mount (XSS-safe); re-clusters on `zoomend`; click pin →
|
||||
local `cluster` state → bottom **sheet** (`.apg-map__sheet`) showing that location's photos via `groupByTime(…, 'day')`.
|
||||
Uses refs (locatedRef/openClusterRef) so Leaflet's once-created callbacks read live state. Screenshots/any GPS
|
||||
item pin (locatedMedia is source-agnostic). VERIFIED: 7 thumbnail pins w/ count badges; clicked one →
|
||||
"New Delhi, India · 4 photos" sheet, date-grouped (Thu Jun 18 / Tue Mar 3).
|
||||
GRID MOSAIC: new `components/views/MosaicGrid.tsx` (date sections via groupByTime 'month'; full-width
|
||||
auto-sliding `SlideHero` (cross-fade through first 6, 3.5s interval, dots) + irregular `.apg-mosaic` CSS grid
|
||||
with varied col/row spans from SPANS pattern; clicking opens lightbox). MapView 'grid' mode now renders it over
|
||||
ALL live media. VERIFIED: 13 month sections, hero + varied tiles, not uniform. `pnpm build` passes (/gallery 181KB).
|
||||
REMAINING (in order): sharing/shared-albums/activity + download links; Recently Deleted password lock + Settings;
|
||||
macOS glass/dark/radius design polish; semantic search (CLIP); video annotations; README. (Plans: docs/feature-plans.json.)
|
||||
- S15: **WAVE 4 DONE & verified** — Sharing + Password lock (both client-side, localStorage; no SQL migration).
|
||||
LOCK: `lib/crypto.ts` (Web Crypto SHA-256 + app salt; hashPassword/verifyPassword); store `lock:{hash}` (persisted
|
||||
to localStorage `apg:lock-hash`) + in-memory `lockUnlocked` + actions setLockPassword/removeLockPassword/unlockLock/
|
||||
relock; RecentlyDeletedView shows a real password lock screen when hash && !unlocked; Sidebar Recently Deleted
|
||||
trailing icon = lock/unlock dynamically; `openSecuritySettings` modal (set/change/remove) from ⋯ menu + trash header.
|
||||
SHARING: types `ShareRecord`; store `shares[]` (localStorage `apg:shares`) + createShare(scope, ids, albumId)/
|
||||
revokeShare; `openShareModal(ids, albumId?)` modal — share selected photo(s) OR tick one/more albums → generates a
|
||||
`?shared=<token>` link, copies to clipboard, Download button; TopToolbar Share icon (when selection>0 or album view);
|
||||
PhotoTile context "Share…" (gated on features.sharing); SharedAlbumsView (cards + copy-link + revoke) + ActivityView
|
||||
(timeline) replace the old placeholders. VERIFIED: set pw→reload→lock screen→unlock OK; cleared test pw; shared 2
|
||||
photos→link generated+stored, Shared Albums card + Activity "You shared '2 Photos' · just now". `pnpm build` passes
|
||||
(/gallery 183KB). NOTE: lock/shares are device-local by design (not pushed to Supabase) — documented.
|
||||
REMAINING: macOS glass/dark/radius design polish (needs visual work from the screenshots); semantic search (CLIP);
|
||||
video annotations; README.
|
||||
- S16: **WAVE 5 DONE & verified** — macOS glass/dark/radius polish (sdk.css). ROOT CAUSE found: `.apg-menu`,
|
||||
`.apg-info`, `.apg-search__recents` used OPAQUE `--apg-bg-elevated`, so backdrop-filter had nothing to blur.
|
||||
Added tokens `--apg-menu-bg` (light rgba(250,250,252,0.78) / dark rgba(44,44,46,0.7)), `--apg-glass-border`
|
||||
(hairline), `--apg-radius-menu:12px`, `--apg-hairline:0.5px`; dark theme made darker (sidebar rgba(22,22,24,0.72)).
|
||||
Applied glass to menu/sidebar(28px blur)/toolbar(24px)/info/search-recents with hairline borders + layered shadow +
|
||||
inner top highlight; `@supports not (backdrop-filter)` → solid fallback. VERIFIED via getComputedStyle in BOTH
|
||||
themes: light app #fff + menu rgba(250,250,252,0.78); dark app #1c1c1e + menu rgba(44,44,46,0.7); both blur(34px)
|
||||
+ radius 12px; screenshot confirms translucent glass dropdown w/ checkmark. (Skipped the dropdown caret/notch —
|
||||
menus open at arbitrary fixed coords so a triangle wouldn't align reliably; noted as future.) `pnpm build` 183KB.
|
||||
NOTE: Playwright screenshot capture was returning stale frames this session (ERR_CACHE) — used computed styles as
|
||||
ground truth. REMAINING: semantic search (CLIP); video annotations; README.
|
||||
- S17: **WAVE 6 — Semantic search (CLIP) DONE & verified.** `apps/web/src/lib/ai/clipProvider.ts`
|
||||
(@huggingface/transformers v3.8.1, `Xenova/clip-vit-base-patch16`, lazy; embedImage via canvas→RawImage→
|
||||
CLIPVisionModelWithProjection; embedText via AutoTokenizer→CLIPTextModelWithProjection; q8/WASM). Wired
|
||||
embedImage+embedText into createDemoAIProvider. AIAnalyzer: 4th `needsEmbedding` gate (m.embedding===undefined) +
|
||||
4th Promise.all leg (own .catch). Store: `semanticResults: MediaId[]|null` + setSemanticResults; setSearch clears it.
|
||||
NEW `components/SemanticSearch.tsx` (headless): debounced on searchQuery, embedText(query) → cosine-rank media w/
|
||||
embeddings (threshold 0.22, top 60) → setSemanticResults; mounted in PhotoGallery. selectors.mediaForView blends
|
||||
keyword hits then semantic-only matches and PRESERVES that relevance order (ranked flag skips chrono sort);
|
||||
useViewMedia passes semanticResults. CSP: connect-src += huggingface.co + *.huggingface.co + *.hf.co (weights
|
||||
redirect to HF Xet CDN us.aws.cdn.hf.co). VERIFIED: 44/45 images embedded (512-D, persisted); query "snow covered
|
||||
mountain" (no keyword match) → top results are snowy peaks/mountains/fog — genuine CLIP ranking. build /gallery 184KB.
|
||||
GOTCHA (same as faces/ocr): first pre-CSP-fix run set embedding=[] → gate wouldn't retry; cleared `embedding` in
|
||||
Supabase, re-embed worked. NOTE: vision + text encoders download separately on first use (slow first time, cached).
|
||||
REMAINING: **full video editor** (annotations + filters + trim + image overlay + mute + add music + Save/Save-as-Copy
|
||||
— user expanded scope; plan = canvas + MediaRecorder + WebAudio, free, no ffmpeg/SharedArrayBuffer); README.
|
||||
- S18: **WAVE 7 — Full video editor DONE & verified.** `lib/videoBake.ts` (canvas + MediaRecorder + WebAudio
|
||||
export, NO ffmpeg/SharedArrayBuffer so CSP intact): offscreen muted video (muted=true for reliable autoplay;
|
||||
audio tapped via WebAudio MediaElementSource), plays trimmed range, draws each frame with ctx.filter
|
||||
(editFilterCss) + image overlay + rasterized annotations (clone live preview <svg> + add viewBox → data URL →
|
||||
Image), captureStream(30) + mixed audio (original unless muted + optional music via gain) → MediaRecorder webm
|
||||
(vp9/opus fallback chain); wall-clock deadline guards against a stalled loop. `components/editor/VideoEditor.tsx`
|
||||
(tabs Adjust/Filters/Trim/Markup/Overlay/Audio; Save/Save-as-Copy(nanoid id + duplicateWithEdits + album picker)/
|
||||
Cancel-with-confirm; progress %). EditState extended: trim{start,end}, overlay{src,x,y,scale}, audio{muted,
|
||||
musicSrc,musicVolume}. PhotoEditor now skips videos (kind==='video'→null); VideoEditor mounted in PhotoGallery.
|
||||
VERIFIED: opened a 30s clip → trim to 2s + Noir filter → Save as Copy → exported "MOV_5004 copy" kind=video
|
||||
mime=video/webm;codecs=vp9,opus DURATION=2 uploaded to Supabase; album picker appeared. `pnpm build` 188KB.
|
||||
===== ALL 13 BACKLOG ITEMS FROM THE BIG USER BATCH ARE DONE & VERIFIED. Only README refresh remains. =====
|
||||
- S19: **2nd big user batch (~19 glitches/features).** Blueprints for the large ones in docs/overhaul-plans.json
|
||||
(persistence root-cause, env-theming, upload, video player, map sheet, missing-features audit).
|
||||
DONE & verified this turn (12): (1) Layout — MEASURED full-width in Library/Collections/Map (240 sidebar + content
|
||||
= viewport); the "80/20 gap" couldn't be reproduced → likely DevTools docked/zoom on user's side. (2) Favorite —
|
||||
clean heart path in icons/index.tsx + RED (.apg-tile__fav #ff3b30 + Lightbox heart red when fav); toggle was fine,
|
||||
the white heart was just invisible. (3) Click a tile now OPENS the lightbox (PhotoTile.onClick; still additive in
|
||||
selection mode); right-click menu adds Open + Select/Deselect and no longer auto-selects. (4) Sidebar = floating
|
||||
rounded glass panel (border-radius var --apg-sidebar-radius default 14px + margin + full border). (5) Search focus
|
||||
ring on .apg-search:focus-within (wraps icon+input). (6) SelectionBar split into Share (openShareModal) + Download
|
||||
(download icon). (7) Lightbox: Edit now shows for VIDEO too, Download uses the download icon, added a Share button;
|
||||
fav button red+aria-pressed. (8) All Albums collapsible (disclosure caret + albumsOpen). (9) Recently Deleted lock
|
||||
icon clickable (Row onTrailingClick → toggleLock: no-pw→settings / unlocked→relock / locked→go unlock). (10) + and
|
||||
Import open a new UploadModal (components/UploadModal.tsx): drag-drop + browse + multi + album picker default=current
|
||||
album; importFiles(files, albumId?) now returns ids + files into the album; image/video only (mediaFromFile
|
||||
validates). (11) Upload processing already auto (addMedia source-albums incl. Screenshots + AIAnalyzer). (12)
|
||||
PERSISTENCE: store.init now treats the backend as the SINGLE SOURCE OF TRUTH (media=loaded.media; only seeds an
|
||||
empty backend once) — fixes "some images missing/stale on refresh"; media never uses localStorage (only lock/shares
|
||||
do, device-local by design). `pnpm build` passes (/gallery 188KB). All verified in-browser via getComputedStyle +
|
||||
interaction probes.
|
||||
STILL PENDING (large, next): map pins square-fit + hover tooltip + draggable resizable bottom-right sheet; custom
|
||||
macOS video player; dedicated Crop menu w/ working rotate/tilt; ENV-driven customization (theme bg/gradient/opacity,
|
||||
sidebar bg+radius, per-feature flags) + README + docs/ENV.md; smooth animations; advanced-features audit; deep test.
|
||||
- S20: **Wave 2 of the 2nd batch DONE & verified (3 items).** (a) MAP pins: `.apg-pin__img` now
|
||||
position:absolute inset:0 + width/height 100% !important (Leaflet's `.leaflet-container img` was leaving it at
|
||||
natural size → non-square; VERIFIED now 43×43 square). Added a hover image tooltip (`.apg-pin__tip` + tip-img +
|
||||
caption, set via DOM in MapView marker 'add'). Sheet is now bottom-RIGHT (width 460, right:12), height set inline
|
||||
from `sheetH` state, with a drag GRIP (`.apg-map__grip` + pointer handlers onHandleDown/Move/Up) to resize toward
|
||||
full (drag down past 0.2 dismisses). VERIFIED drag 474→758px. (b) CROP: removed the `tab==='crop'` exclusion on
|
||||
transformCss in PhotoEditor so rotate/straighten/flip PREVIEW LIVE in the Crop tab (normal crop unaffected since
|
||||
no-transform=no-op). VERIFIED straighten 12° → matrix rotate+cover-scale. (c) ENV CUSTOMIZATION: store `ThemeTokens`
|
||||
(bg/elevated/sidebarBg/text light+dark, sidebarRadius, accent — any color/rgba/gradient) + GalleryConfig.themeTokens;
|
||||
PhotoGallery maps tokens→CSS vars per resolvedTheme into rootStyle; exported ThemeTokens. Demo
|
||||
`apps/web/src/lib/galleryConfig.ts` reads NEXT_PUBLIC_APG_* (feature flags + theme tokens + accent/radius/theme);
|
||||
GalleryClient applies env over defaults. Docs: `docs/ENV.md` (full var list + examples) + README Customization
|
||||
section + .env.example. `pnpm build` passes (/gallery 189KB). 15/19 of the 2nd batch done.
|
||||
STILL PENDING: custom macOS video player; smooth animations; advanced-features audit; deep test.
|
||||
- S21: **Wave 3 (final) of the 2nd batch DONE & verified — ALL 19 items complete.** (a) CUSTOM VIDEO PLAYER:
|
||||
`components/VideoPlayer.tsx` (glass transport: play/pause, pointer-drag scrubber w/ buffered+played, time,
|
||||
mute+volume, PiP, fullscreen, auto-hide, keyboard) replaces native controls in Lightbox; new icons pause/volume/
|
||||
mute/expand/pip. VERIFIED custom controls + no native + play/pause. (b) ANIMATIONS: tile hover zoom (hover:hover),
|
||||
`.apg-scroll` view fade-in keyframe, prefers-reduced-motion guard. (c) FEATURE AUDIT documented in ROADMAP §7b
|
||||
(prioritized backlog). (d) DEEP TEST = ultracode adversarial review workflow (3 dims × find→refute, 17 agents);
|
||||
correctness/security mostly clean, refuted false alarms (place-name XSS, video edit routing, sheet stale-closure).
|
||||
FIXED all confirmed: CRITICAL VideoPlayer arrows double-fired the Lightbox window nav → owned keys now
|
||||
e.nativeEvent.stopImmediatePropagation() (VERIFIED: ArrowRight seeks 1→2.9s, lightbox does NOT navigate); HIGH
|
||||
pointercancel leaks on VideoPlayer scrubber + MapView grip → added pointercancel cleanup; HIGH init() double-seed
|
||||
under Strict Mode/remount → `initedRef` guard runs init once; MED scrubber seek when duration=0 → guarded;
|
||||
MED selection-mode click-can't-open → mitigated by existing double-click/Enter/right-click Open; LOW UploadModal
|
||||
now uses isAcceptedMediaFile (matches backend allow-list); LOW accent env redundancy removed; LOW themeToken CSS
|
||||
values sanitized (reject url()/expression()/breakout). `pnpm build` passes (/gallery 191KB).
|
||||
===== 2ND BATCH (19 items: layout, favorite, select/open, sidebar radius, search focus, bottom bar, lightbox
|
||||
buttons, collapsible albums, lock icon, upload, persistence, map pins+sheet, crop tilt, env customization + ENV.md,
|
||||
video player, animations, features audit, deep test) ALL DONE & VERIFIED. Remaining known limitations: sharing/lock
|
||||
are device-local; advanced backlog in ROADMAP §7b (zoom/pan, metadata editing, slideshow, etc.) for future waves. =====
|
||||
- S-versions (2026-07-02): **Versioning + Audit Log + Comments DONE & verified** (photos AND videos).
|
||||
DATA: `types.ts` — `MediaItem.versions?: MediaVersion[]` (v1 = original, never overwritten; each save appends)
|
||||
+ `MediaItem.comments?: MediaComment[]`; new `MediaVersion`/`MediaComment` interfaces; `ViewId` += `'versions'`.
|
||||
STORE (`store/store.ts`): `addVersion(id, patch, changes)` (seeds v1 from the pre-edit snapshot on first edit,
|
||||
then appends), `restoreVersion(id, versionId)` (sets live fields to the chosen version + appends a "Restored
|
||||
version N" audit entry), `addComment(id, text, author?)`, `deleteComment(id, commentId)` — all persist().
|
||||
AUDIT LOG helper: `lib/versions.ts` `summarizeEdits(edits): string[]` (Cropped / Rotated / Filter: x / N
|
||||
annotations / Trimmed / Muted / etc.). EDITOR WIRING: `PhotoEditor.save(false)` & `VideoEditor.save(false)`
|
||||
now call `addVersion(...)` instead of `updateMedia` (Save-as-Copy path unchanged). UI: `InfoPanel.tsx` gained a
|
||||
**Version history** section (thumbnail per version w/ live CSS filter+transform preview, timestamp, expandable
|
||||
"What changed" audit list, Restore button on non-current) + **Comments** section (avatar/author/time list, delete
|
||||
on hover, name+textarea+Post form; author cached in localStorage `apg:comment-author`; Cmd/Ctrl+Enter posts).
|
||||
SIDEBAR: Utilities → "Versions & Audit" (icon clock, view `versions`). NEW VIEW: `views/VersionsView.tsx`
|
||||
(audit browser — every item with versions or comments; click → openLightbox+Info). ViewRouter case `versions`.
|
||||
CSS: `styles/sdk.css` apg-info__section / apg-versions / apg-version__* / apg-comments / apg-comment* /
|
||||
apg-comment-form* / apg-audit-* + `.apg-btn--small`/`.apg-btn:disabled`. PERSISTENCE: stored inline on MediaItem →
|
||||
Supabase JSONB (`data: m`) automatically; no schema change needed. VERIFIED: (a) unit test vs built dist —
|
||||
original preserved as v1, v2/v3 appended with audit logs, live src = latest, restore works, comment add/delete +
|
||||
author OK; (b) live UI on :3007 — sidebar item present, empty state, Info shows both sections, posted a comment
|
||||
(Davy → rendered w/ time), audit browser lists the item ("test-invoice.png · 1 version · 1 comment"). typecheck +
|
||||
`pnpm build` pass. NOTE: dev :3000 was occupied by a stale 500 server; tested on :3007.
|
||||
|
||||
- S-advanced (2026-07-07): **Advanced video editor + object smart albums + upload/analysis/versioning/persistence fixes + deploy guide.** Diagnosed via a 6-agent workflow (root causes in that run's journal).
|
||||
VERSIONING BUGS FIXED: (1) reload-strip — `lib/media.ts createMediaItem` now copies `versions`+`comments`
|
||||
(the localStorage load path dropped them → "stuck at v1" after refresh). (2) Save-as-Copy — `store.duplicateWithEdits(sourceId,patch,changes?)` now seeds the copy's OWN [v1 Original, v2 edit] history + resets comments (was inheriting/blank). Editors hoist `changes` + pass to duplicateWithEdits; catch fallbacks now addVersion/duplicateWithEdits (not applyEdit) so history is never lost.
|
||||
PERSISTENCE: `docs/supabase-setup.sql` now CREATES the public `media` bucket (was never created → uploads silently fell back to non-durable URLs). `store.importFiles` + `Camera` never persist a bare `blob:` URL — convert to a durable data URL (`blobToDataUrl` now exported from lib/media).
|
||||
UPLOAD: `UploadModal` has an explicit "Choose files" button + visually-hidden input (not `hidden` attr) + resets value so re-picking same file fires onChange.
|
||||
ANALYSIS-ON-UPLOAD: `AIAnalyzer` now collects pending NEWEST-FIRST + drains in rounds (items imported mid-run are caught) → uploads analyze like captures; also calls `syncObjectAlbums()` on completion.
|
||||
OBJECT SMART ALBUMS: `lib/smartAlbums.ts objectSmartAlbums(media,now,minCount)` (one live `sys:obj:<label>` smart album per detected label, rule {field:'object',op:'contains'}); `store.syncObjectAlbums()` (interface + impl near rebuildPeople; called at end of init()); Sidebar "Objects" collapsible section (`sys:obj:*`); CollectionsView Objects cards now `setView('sys:obj:'+label)`. Membership live via resolveSmartAlbum; albums not persisted (regenerated).
|
||||
VIDEO EDITOR (heavy): types `EditState.segments/overlays/posterTime/export` + extended `audio` (originalVolume/fadeIn/fadeOut); new `VideoSegment/VideoOverlay/OverlayKeyframe`. New `lib/videoTimeline.ts` (normalizeSegments/outputDuration/sourceToOutputTime/videoOutputSize/resolveOverlays/sampleOverlay — pure, shared by bake+preview, exported from index). `lib/videoBake.ts` REWRITTEN as a timeline engine: ONE continuous MediaRecorder driven across segments (multi-trim + per-seg speed), crop+90°rot+flip geometry, CSS filter, overlays (image+text) keyframe-interpolated at output time, watermark, legacy overlay/annotations, WebAudio (original vol/mute + music + master fade), poster grab, export quality. `VideoEditor.tsx` REBUILT with tabs Trim&Split / Crop&Rotate / Overlays&Text / Filters / Adjust / Markup / Audio / Export + live keyframed overlay preview + surfaced export errors. `summarizeEdits` extended.
|
||||
DEPLOY: `docs/DEPLOY.md` (Vercel Root Dir=apps/web + Supabase free tier, env vars, gotchas: bucket public, CSP dynamic, Vercel 4.5MB body cap for Gemini). VERIFIED: `pnpm typecheck` + `pnpm build` pass (/gallery 198KB); 17/17 logic unit tests vs built dist (versioning increment/copy-history, objectSmartAlbums, timeline+keyframe interpolation, summarizeEdits); live browser (Edge) — 0 console errors, Objects sidebar live (Person/Car/Chair…), upload modal Choose-files, video editor all 8 tabs + text overlay live preview + keyframe UI. Did NOT git commit (user commits).
|
||||
|
||||
- S-deploy-ai (2026-07-07): **Manual (no-Git) deploy guide + swappable free/local AI backend.**
|
||||
DEPLOY: `docs/DEPLOY-MANUAL.md` — 3 no-Git paths: (A) Vercel CLI `vercel --prod` from repo root, Root Dir=apps/web (recommended; keeps SSR+nonce CSP+AI route); (B) self-host Node (`pnpm build` → `pnpm --filter web start` on Render/Railway/VPS); (C) static export to any host — requires `output:'export'` + DISABLING the nonce middleware/layout headers (security trade-off, no server AI route). SDK ships as source via transpilePackages so "whole project" = build the demo (no npm publish).
|
||||
AI: `apps/web/src/app/api/ai/edit/route.ts` REFACTORED to a pluggable backend via `AI_EDIT_PROVIDER` (auto|local|huggingface|gemini|none). auto = local→huggingface→gemini. `local` = own Stable Diffusion (AUTOMATIC1111/Forge/SD.Next `/sdapi/v1/img2img`, env LOCAL_SD_URL+denoise/steps/sampler; free+private). `huggingface` = free Inference API instruct-pix2pix (HF_API_TOKEN, HF_IMAGE_MODEL; 503 cold-start handled). `gemini` kept (needs BILLED key for image). MAX_BASE64 lowered to 4M (under Vercel 4.5MB). Server-side fetches → no CSP change. KEY POINT documented in `docs/AI-SETUP.md`: analysis (objects/faces/OCR/embeddings via TF.js/face-api/tesseract/transformers) + Remove Background (@imgly) already run FREE in-browser, no key — only generative pixel edits needed a backend. `.env.example` rewritten with Supabase + all AI options. typecheck + build pass. Did NOT git commit.
|
||||
|
||||
## Key files
|
||||
- Advanced video editor (S-advanced): `packages/photo-sdk/src/lib/videoBake.ts` + `lib/videoTimeline.ts` + `components/editor/VideoEditor.tsx`.
|
||||
- AI edit backend (S-deploy-ai): `apps/web/src/app/api/ai/edit/route.ts` (env AI_EDIT_PROVIDER).
|
||||
- SDK: `packages/photo-sdk/src/{components,store,adapters,ai,lib,icons,styles}`.
|
||||
- Demo: `apps/web/src/{app,components,lib}` (app/api/ai/edit = Gemini proxy; middleware = nonce CSP).
|
||||
- Camera (S4): `packages/photo-sdk/src/components/Camera.tsx` (store flag `cameraOpen`).
|
||||
|
||||
## Do NOT
|
||||
- git commit/push. Commit secrets. Rely on localStorage as the final backend (Supabase is the target).
|
||||
Reference in New Issue
Block a user