Files
2026-07-18 01:34:52 +05:30

35 KiB
Raw Permalink Blame History

Build status & session log

Run

pnpm install && pnpm devhttp://localhost:3000 (landing → Open Gallery → /gallery). pnpm build to ship.

Current state (2026-06-29)

Phases 13 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; 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 DONEapps/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 & verifiedcomponents/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 & verifiedapps/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 + 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.tsMediaItem.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 buildpnpm --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).