35 KiB
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). 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; storecameraOpen/openCamera/closeCamera; toolbar 📷 button). Verified modal UI (no camera device in test env; works with a real webcam).pnpm buildpasses (/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 gainedimportFiles/uploadBlob(uploads on import + camera capture); TopToolbar/LibraryView/Camera route through them; GalleryClient uses Supabase whenNEXT_PUBLIC_SUPABASE_*set (else localStorage). CSP (middleware) now allowshttps://*.supabase.co(connect+img+media). Bucketmediamade public. Creds inapps/web/.env.local(gitignored). Adapter degrades gracefully if tables missing (probe→warn→seed). USER MUST rundocs/supabase-setup.sqlonce (SQL Editor) + reload to activate persistence.pnpm buildpasses (/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 buildpasses. 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.tsflattens crop+flip+filter+vignette+rotation/straighten to a JPEG on Done and re-maps annotations;PhotoEditor.save()bakes whenhasGeometry(edits)and uploads viauploadBlob(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 (objecteMdEj_Ow32-…jpg), displays cropped; 0 console errors;pnpm buildpasses. 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 cleareditsbut 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 buildpass; 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) increateDemoAIProvider.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:andhttps://staticimgly.comto 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 buildpasses (/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 fromcdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.15/model, added to connect-src). Wired asdetectFacesin createDemoAIProvider. SDK:lib/cluster.ts(greedy euclidean clustering, threshold 0.55, biggest-face-first); storerebuildPeople()(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.tsomitted 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 buildpasses (/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
ultracodeWorkflow 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 asocrin createDemoAIProvider. SDK:types.tsMediaItem.ocrText + SmartRule 'hasText' + ViewIdsys:${string};lib/text.ts(NEW) sanitizeOcrText (control-char strip via RegExp-from-string, ws-collapse, 10k cap);smartAlbums.tshasText eval +sys:documentsalbum;selectors.tssearchMedia haystack += ocrText;AIAnalyzer.tsx3rd OCR leg (ownocrText===undefinedgate, 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 buildpasses (/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 gateocrText===undefinedwon'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 (newmoveToAlbumPicker, 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 buildpasses (/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.rebuildPeoplenow also builds pet groups from objectLabels with STABLE idspet:<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 generalizedpromptAlbumName(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 buildpasses (/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.
- Birds + Horses; renamed human → "Davy" (persisted to gallery_people); Objects lists all 14 detected types,
click "boat" → 2 photos + "Object: boat" banner.
-
S14: WAVE 3 DONE & verified — Map redesign. PINS:
selectors.clusterByLocation(items, precision)(round coords; precision rises with zoom) +LocationCluster. MapView now renders LeafletdivIconTHUMBNAIL pins (.apg-pinimg + count badge + tail; CSS in sdk.css) — static html only, img.src + count set via DOM after mount (XSS-safe); re-clusters onzoomend; click pin → localclusterstate → bottom sheet (.apg-map__sheet) showing that location's photos viagroupByTime(…, '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: newcomponents/views/MosaicGrid.tsx(date sections via groupByTime 'month'; full-width auto-slidingSlideHero(cross-fade through first 6, 3.5s interval, dots) + irregular.apg-mosaicCSS 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 buildpasses (/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); storelock:{hash}(persisted to localStorageapg:lock-hash) + in-memorylockUnlocked+ actions setLockPassword/removeLockPassword/unlockLock/ relock; RecentlyDeletedView shows a real password lock screen when hash && !unlocked; Sidebar Recently Deleted trailing icon = lock/unlock dynamically;openSecuritySettingsmodal (set/change/remove) from ⋯ menu + trash header. SHARING: typesShareRecord; storeshares[](localStorageapg: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 buildpasses (/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__recentsused 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 build183KB. 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.
- 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.)
-
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: 4thneedsEmbeddinggate (m.embedding===undefined) + 4th Promise.all leg (own .catch). Store:semanticResults: MediaId[]|null+ setSemanticResults; setSearch clears it. NEWcomponents/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; clearedembeddingin 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 previewcomponents/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 build188KB. ===== 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 buildpasses (/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__imgnow position:absolute inset:0 + width/height 100% !important (Leaflet's.leaflet-container imgwas 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 fromsheetHstate, 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 thetab==='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: storeThemeTokens(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. Demoapps/web/src/lib/galleryConfig.tsreads 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 buildpasses (/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-scrollview 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 →initedRefguard 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 buildpasses (/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[]; newMediaVersion/MediaCommentinterfaces;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.tssummarizeEdits(edits): string[](Cropped / Rotated / Filter: x / N annotations / Trimmed / Muted / etc.). EDITOR WIRING:PhotoEditor.save(false)&VideoEditor.save(false)now calladdVersion(...)instead ofupdateMedia(Save-as-Copy path unchanged). UI:InfoPanel.tsxgained 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 localStorageapg:comment-author; Cmd/Ctrl+Enter posts). SIDEBAR: Utilities → "Versions & Audit" (icon clock, viewversions). NEW VIEW:views/VersionsView.tsx(audit browser — every item with versions or comments; click → openLightbox+Info). ViewRouter caseversions. CSS:styles/sdk.cssapg-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 buildpass. 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 createMediaItemnow copiesversions+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 hoistchanges+ pass to duplicateWithEdits; catch fallbacks now addVersion/duplicateWithEdits (not applyEdit) so history is never lost. PERSISTENCE:docs/supabase-setup.sqlnow CREATES the publicmediabucket (was never created → uploads silently fell back to non-durable URLs).store.importFiles+Cameranever persist a bareblob:URL — convert to a durable data URL (blobToDataUrlnow exported from lib/media). UPLOAD:UploadModalhas an explicit "Choose files" button + visually-hidden input (nothiddenattr) + resets value so re-picking same file fires onChange. ANALYSIS-ON-UPLOAD:AIAnalyzernow collects pending NEWEST-FIRST + drains in rounds (items imported mid-run are caught) → uploads analyze like captures; also callssyncObjectAlbums()on completion. OBJECT SMART ALBUMS:lib/smartAlbums.ts objectSmartAlbums(media,now,minCount)(one livesys: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 nowsetView('sys:obj:'+label). Membership live via resolveSmartAlbum; albums not persisted (regenerated). VIDEO EDITOR (heavy): typesEditState.segments/overlays/posterTime/export+ extendedaudio(originalVolume/fadeIn/fadeOut); newVideoSegment/VideoOverlay/OverlayKeyframe. Newlib/videoTimeline.ts(normalizeSegments/outputDuration/sourceToOutputTime/videoOutputSize/resolveOverlays/sampleOverlay — pure, shared by bake+preview, exported from index).lib/videoBake.tsREWRITTEN 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.tsxREBUILT with tabs Trim&Split / Crop&Rotate / Overlays&Text / Filters / Adjust / Markup / Audio / Export + live keyframed overlay preview + surfaced export errors.summarizeEditsextended. 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 buildpass (/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 CLIvercel --prodfrom repo root, Root Dir=apps/web (recommended; keeps SSR+nonce CSP+AI route); (B) self-host Node (pnpm build→pnpm --filter web starton Render/Railway/VPS); (C) static export to any host — requiresoutput:'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.tsREFACTORED to a pluggable backend viaAI_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).geminikept (needs BILLED key for image). MAX_BASE64 lowered to 4M (under Vercel 4.5MB). Server-side fetches → no CSP change. KEY POINT documented indocs/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.examplerewritten 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 flagcameraOpen).
Do NOT
- git commit/push. Commit secrets. Rely on localStorage as the final backend (Supabase is the target).