{ "summary": "Investigate + plan the larger gallery requests in parallel: persistence/refresh audit, env-driven theming, upload pipeline, custom video player, map sheet, missing-features audit", "agentCount": 6, "logs": [], "result": { "plans": [ { "area": "Image Loading Race Condition & Persistence Architecture", "findings": "ROOT CAUSE: Three interlocking race conditions cause \"on refresh, sometimes not all images show\":\n\n1. **Seed Data Overwrite (store.ts init)**: Store initializes with 44 seed photos via `initialMedia`, but `init()` runs async. When backend loads, line 306 executes `media: loaded.media.length ? loaded.media : get().media`. If backend returns empty or partial data, it overwrites the seed. No fallback merge exists.\n\n2. **localStorage Caches Stale Snapshots**: When Supabase adapter is used but backend is uninitialized (tables don't exist), `init()` returns null (line 313-314 in store.ts). The app then calls `persist()` immediately (line 314), writing the current in-memory state to localStorage. On next reload, localStorage loads that stale snapshot synchronously, defeating the seed entirely. localStorage wins because it's sync; Supabase load is async.\n\n3. **Race Between Seed Init and Backend Load**: PhotoGallery.tsx (lines 91-99) creates the store with seed photos in `initialMedia`. GalleryRoot (line 141) calls `init(adapter, ai)` async. If Supabase returns in 200ms while localStorage is persisting in parallel, both race to set state. The persist() function (store.ts 252-257) debounces saves every 400ms, creating windows where stale localStorage state can be written before a fresh backend load completes.\n\n4. **No Seeding for Fresh Backend**: supabaseAdapter.ts (lines 39-64) returns only what exists in Postgres. If backend is empty (fresh user, failed migration), there's no fallback to seed. App shows blank gallery instead of demo content.\n\nEVIDENCE:\n- store.ts lines 306, 314: conditional media load with no merge strategy\n- store.ts lines 249-257: persist() debounces at 400ms, allowing race windows\n- localStorage.ts lines 20-24: synchronous load on component mount\n- GalleryClient.tsx line 43: seed passed as initialMedia but has no protective logic\n- supabaseAdapter.ts line 46: returns null when tables don't exist, no fallback\n\nARCHITECTURAL ISSUE: Storage adapter pattern allows multiple sources (seed, localStorage, Supabase) to race. Current code treats localStorage and backend as equals, but they should be hierarchical (backend=source-of-truth, localStorage=transient cache for client settings only).\n", "plan": "BACKEND-ONLY PERSISTENCE: Restructure to make Supabase (or any backend adapter) the single source of truth for media/albums/people, while localStorage holds only tiny client-only settings (lock hash, shares).\n\nIMPLEMENTATION:\n1. **Remove localStorage dependency for media/albums/people** in store.ts:\n - Move LOCK_KEY and SHARES_KEY out of the `persist()` function (keep them client-local)\n - Modify `persist()` to only save via the backend adapter, never to localStorage\n - Remove the localStorage fallback logic that writes stale state\n\n2. **Fix init() to merge seed with backend data** in store.ts:\n - When adapter.load() returns null (backend uninitialized), don't just keep seed in memory\n - When adapter.load() returns partial data, merge with seed (backend additions + seed defaults)\n - Call persist() to write the merged state back to backend for future loads\n - This seeds the backend on first initialization\n\n3. **Create a separate client-settings adapter** in packages/photo-sdk/src/adapters/clientSettings.ts:\n - Handle only `lock` (hash) and `shares` (ShareRecord[])\n - Uses localStorage as before (these are device-local, not synced to backend)\n - Loaded/saved independently from the main adapter\n\n4. **Update store.ts init()** to:\n - Load main adapter (media/albums/people) from backend\n - Load client-settings adapter separately for lock+shares\n - Merge any seed data with backend on first run\n - Only use backend for media/albums/people persist operations\n\n5. **Update GalleryClient.tsx**:\n - Still pass seed as initialMedia (acts as fallback only)\n - init() logic handles merge with backend if backend is empty or partial\n - No special seed-handling logic needed in the component\n\n6. **Add seed-on-empty logic to supabaseAdapter.ts** (optional but recommended):\n - Add an optional `seedData?: PersistedState` parameter to createSupabaseAdapter\n - On first successful load that returns empty data, insert seed and return it\n - Prevents blank gallery on fresh backends\n\nFILES TO CHANGE:\n- packages/photo-sdk/src/store/store.ts (init, persist, lock/shares handling)\n- packages/photo-sdk/src/adapters/localStorage.ts (remove media/albums/people, keep only client-settings reference)\n- packages/photo-sdk/src/adapters/clientSettings.ts (NEW - tiny adapter for lock+shares only)\n- apps/web/src/lib/adapters/supabaseAdapter.ts (optional: add seedData parameter + seed-on-empty logic)", "files": [ { "path": "packages/photo-sdk/src/store/store.ts", "change": "CRITICAL: Refactor init() and persist() to use backend as single source of truth. (1) Split persist() into two: persistBackend() for media/albums/people (uses adapter), and persistClientSettings() for lock/shares (uses localStorage). (2) Fix init() to merge loaded backend data with initialMedia seed if backend is empty or partial, then call persistBackend() to write merged state. (3) Remove the ternary fallback on line 306 that discards partial backend data. (4) Ensure persist() calls only trigger persistBackend() (async to adapter), never write media/albums/people to localStorage. Keep lock/shares in client-only localStorage. Full file changes required at lines 249-322 (init and persist functions)." }, { "path": "packages/photo-sdk/src/adapters/clientSettings.ts", "change": "NEW FILE: Minimal adapter for client-only settings (lock hash + shares). Exports createClientSettingsAdapter() that uses localStorage only, similar to localStorage.ts but scoped to { lock: { hash: string | null }, shares: ShareRecord[] }. Used in parallel with main backend adapter for device-local state." }, { "path": "packages/photo-sdk/src/adapters/types.ts", "change": "OPTIONAL: Add ClientSettingsAdapter interface if creating separate adapter, or reuse StorageAdapter pattern. Keep existing StorageAdapter interface unchanged." }, { "path": "apps/web/src/lib/adapters/supabaseAdapter.ts", "change": "ENHANCEMENT (optional but recommended): Add optional seedData?: PersistedState parameter to createSupabaseAdapter(). In load(), if the backend returns no data (empty gallery_media), insert seedData and return it, so fresh backends show demo content. Prevents blank app on first user load. Around lines 18, 39-64." }, { "path": "apps/web/src/components/GalleryClient.tsx", "change": "MINOR: Pass seed to PhotoGallery as before. No logic changes needed here—the store init() merge logic handles fallback to seed. seed persists as initialMedia for use in init()." } ] }, { "area": "ENV-driven customization system for PhotoGallery SDK", "findings": "\nCurrent state analysis:\n\n**PhotoGallery Component** (packages/photo-sdk/src/components/PhotoGallery.tsx):\n- Accepts props: photos, albums, adapter, ai, theme, accentColor, borderRadius, features, showWindowChrome, title\n- GalleryConfig in store includes: features, accentColor, borderRadius, showWindowChrome, title\n- GalleryFeatures flags: editor, camera, ai, map, import, export, sharing (all boolean)\n- Currently only accentColor and borderRadius are theme-related; CSS vars are computed inline via rootStyle\n- Inline styles set --apg-accent, --apg-radius, --apg-radius-sm/lg/xl\n\n**GalleryClient Demo** (apps/web/src/components/GalleryClient.tsx):\n- Hardcodes features (all true), theme (system), accentColor, and adapter\n- Reads NEXT_PUBLIC_SUPABASE_* for backend but nothing for theme customization\n- No env vars for styling or feature flags currently\n\n**CSS Design System** (packages/photo-sdk/src/styles/sdk.css):\n- Uses CSS custom properties --apg-* for colors, shadows, spacing, radius\n- Light theme (default): --apg-bg, --apg-bg-content, --apg-bg-elevated, --apg-sidebar-bg (with rgba backdrop)\n- Dark theme [data-theme='dark']: darker backgrounds + adjusted text colors\n- Semi-dark theme: dark sidebar + light content area\n- All shadows, separators, text colors, hovers have CSS vars\n- Supports gradient usage (e.g., .apg-pinned-card uses linear-gradient)\n- No opacity vars defined; opacity is inline in rgba() calls\n\n**Root cause**: Config is component-prop-driven only; no bridge to read NEXT_PUBLIC_* env vars in the demo; CSS customization is accent/radius only; no theme background, sidebar, gradient, or opacity controls via env.\n\n**Desired end state**: Developers should set NEXT_PUBLIC_APG_* env vars (e.g., NEXT_PUBLIC_APG_EDITOR=true, NEXT_PUBLIC_APG_BG_LIGHT=#ffffff, NEXT_PUBLIC_APG_SIDEBAR_BG_DARK=rgba(...)) and have them automatically apply to the gallery without prop drilling.\n", "plan": "\n**Phase 1: Extend GalleryConfig with theme tokens**\n1. Create new interface `ThemeTokens` in packages/photo-sdk/src/store/store.ts:\n - bgLight, bgDark (main backgrounds)\n - bgContentLight, bgContentDark (content area overlays)\n - bgElevatedLight, bgElevatedDark (raised surfaces like cards)\n - sidebarBgLight, sidebarBgDark (sidebar glass backdrop)\n - textLight, textDark, textSecondaryLight, textSecondaryDark\n - sidebarBorderRadiusLight, sidebarBorderRadiusDark (border-radius on sidebar)\n - accentOpacity (0-1 for accent transparency)\n - gradientBgLight, gradientBgDark (full gradient strings for pinned cards/hero)\n - Optional: separatorLight, separatorDark, hoverLight, hoverDark, shadowLight, shadowDark\n\n2. Extend GalleryConfig to include optional `themeTokens?: Partial`\n\n3. In PhotoGallery.tsx GalleryRoot component, compute merged CSS vars object:\n - Start with defaults from theme (light/dark/semi-dark)\n - Layer on themeTokens overrides\n - Set as inline style on .apg root element (spread into rootStyle)\n\n**Phase 2: Create env-to-config helper in demo**\n1. Create new file: apps/web/src/lib/galleryConfig.ts\n - Export `getGalleryConfigFromEnv()` function\n - Read all NEXT_PUBLIC_APG_* vars via process.env\n - Parse boolean features (NEXT_PUBLIC_APG_EDITOR etc.)\n - Parse color strings for theme tokens\n - Parse opacity as float\n - Parse gradient strings as-is\n - Return merged GalleryConfig ready for \n\n2. Update apps/web/src/components/GalleryClient.tsx:\n - Call getGalleryConfigFromEnv() to build config\n - Pass config + themeTokens to \n - Keep hardcoded defaults as fallbacks\n\n**Phase 3: Update CSS to accept theme token vars**\n1. Modify packages/photo-sdk/src/styles/sdk.css:\n - Keep existing --apg-* vars as-is (for backward compat)\n - Add new optional theme token vars (--apg-theme-bg-light, etc.)\n - Use CSS cascading: if --apg-theme-bg-light is set, it overrides --apg-bg in light mode\n - Use @supports or fallback pattern for gradients vs static colors\n\n**Phase 4: Documentation**\n1. Create docs/ENV.md:\n - Full alphabetical list of NEXT_PUBLIC_APG_* vars\n - Categories: Features, Colors (light/dark), Sidebar, Opacity, Gradients\n - Example .env.local file\n - Defaults for each\n - Value formats (hex, rgb(), rgba(), gradients)\n - Test examples\n\n2. Update README.md:\n - Link to docs/ENV.md\n - Add \"Customization via environment\" section\n - Show quick example\n\n**Phase 5: Type safety**\n1. Create types/env.ts:\n - Export EnvConfig type with all NEXT_PUBLIC_APG_* keys\n - Build type-safe accessors for env reads\n", "files": [ { "path": "packages/photo-sdk/src/store/store.ts", "change": "Extend GalleryConfig with optional ThemeTokens interface. Add new ThemeTokens type with bgLight, bgDark, bgContentLight, bgContentDark, bgElevatedLight, bgElevatedDark, sidebarBgLight, sidebarBgDark, textLight, textDark, textSecondaryLight, textSecondaryDark, accentOpacity, sidebarBorderRadiusLight, sidebarBorderRadiusDark, gradientBgLight, gradientBgDark, separatorLight, separatorDark, hoverLight, hoverDark. Update GalleryConfig interface to include themeTokens?: Partial. All colors support hex, rgb(), rgba(), and gradient strings. Keep backward compat with accentColor and borderRadius." }, { "path": "packages/photo-sdk/src/components/PhotoGallery.tsx", "change": "Update GalleryRoot component to merge themeTokens with resolved theme and compute final CSS vars. In the rootStyle object, add computed vars like --apg-theme-bg-light, --apg-theme-bg-dark, --apg-theme-sidebar-bg-light, --apg-theme-sidebar-bg-dark, --apg-theme-accent-opacity, --apg-theme-gradient-light, --apg-theme-gradient-dark, etc. These cascade into .apg element and are ready for CSS to consume. Keep all existing radius/accent vars for backward compat." }, { "path": "packages/photo-sdk/src/styles/sdk.css", "change": "Add optional theme token CSS var handlers. In .apg light theme block, add fallback patterns: --apg-bg: var(--apg-theme-bg-light, #ffffff); for each customizable color. Similarly for [data-theme='dark']. For gradients, use .apg-pinned-card background: var(--apg-theme-gradient-light, linear-gradient(...)); Support var(--apg-theme-accent-opacity) in rgba values. Keep all existing --apg-* vars as-is for backward compat. No breaking changes." }, { "path": "apps/web/src/lib/galleryConfig.ts", "change": "Create new helper file. Export getGalleryConfigFromEnv() function that reads process.env.NEXT_PUBLIC_APG_* vars and builds config. Parse features: NEXT_PUBLIC_APG_EDITOR, NEXT_PUBLIC_APG_CAMERA, NEXT_PUBLIC_APG_AI, NEXT_PUBLIC_APG_MAP, NEXT_PUBLIC_APG_IMPORT, NEXT_PUBLIC_APG_EXPORT, NEXT_PUBLIC_APG_SHARING (parse 'true'/'false' strings). Parse theme tokens: NEXT_PUBLIC_APG_BG_LIGHT, NEXT_PUBLIC_APG_BG_DARK, NEXT_PUBLIC_APG_BG_CONTENT_LIGHT, etc. Parse NEXT_PUBLIC_APG_ACCENT, NEXT_PUBLIC_APG_BORDER_RADIUS, NEXT_PUBLIC_APG_ACCENT_OPACITY. Return merged GalleryConfig with themeTokens. All vars optional with sensible defaults." }, { "path": "apps/web/src/components/GalleryClient.tsx", "change": "Update to use getGalleryConfigFromEnv(). Import the helper and call it before rendering PhotoGallery. Merge the returned config with hardcoded defaults. Pass both config features, accentColor, borderRadius and the themeTokens object to PhotoGallery. Keep adapter and ai logic unchanged. Fallback logic: env overrides hardcoded defaults, but missing env vars use hardcoded values." }, { "path": "docs/ENV.md", "change": "Create comprehensive environment variable documentation. List all NEXT_PUBLIC_APG_* vars in categories: Features (editor, camera, ai, map, import, export, sharing), Color Theme (bg-light, bg-dark, bg-content-*, sidebar-bg-*, text-*, accent-opacity, sidebar-border-radius-*), Gradients (gradient-bg-light, gradient-bg-dark). For each var: name, type (boolean/color/string/number), default value, example. Include full example .env.local showing all vars. Show light/dark/semi-dark fallback logic. Explain CSS var mapping." }, { "path": ".env.example", "change": "Update root .env.example to show all NEXT_PUBLIC_APG_* vars for feature flags and theme customization. Keep Supabase and Gemini sections. Add new section '# Photo Gallery SDK Customization' with commented examples of all env vars with defaults. Format: NEXT_PUBLIC_APG_EDITOR=true, NEXT_PUBLIC_APG_BG_LIGHT=#ffffff, NEXT_PUBLIC_APG_SIDEBAR_BG_DARK=rgba(22,22,24,0.72), etc." }, { "path": "packages/photo-sdk/src/types/index.ts", "change": "Check if exists; add export for ThemeTokens type from store.ts so consumers can reference it." }, { "path": "README.md", "change": "Add section 'Customization via Environment Variables' before 'Using the SDK in your own app'. Show quick example of setting NEXT_PUBLIC_APG_EDITOR=false and NEXT_PUBLIC_APG_BG_DARK=#1a1a1a. Link to docs/ENV.md for full list. Mention backward compat with accentColor/borderRadius props." } ] }, { "area": "Upload Flow Design", "findings": "CURRENT STATE:\nThe SDK already has distributed upload capability via TopToolbar.tsx (line 188-198) and LibraryView.tsx (line 51-58), but no unified modal. Both use raw file input elements with accept=\"image/*,video/*\" and call importFiles(files). Processing happens in store.ts importFiles (line 342-363) which uses mediaFromFile from lib/media.ts to validate, read metadata, and optionally upload via adapter.putBlob. AIAnalyzer.tsx automatically processes newly-added media in the background (objects, faces, OCR, embeddings). Source classification (screenshot detection) happens via classify.ts automatically on import.\n\nROOT CAUSES / GAPS:\n1. No dedicated upload modal — file picker is hidden inline in two places\n2. Album selection not exposed at upload time — files are auto-filed only by source (Camera/Screenshots albums)\n3. Drag-drop is only in LibraryView, not available from other views\n4. No visual feedback about what file types are accepted\n5. No album picker defaulting to current album\n6. Processing happens transparently (no UI about which stage is running)\n\nACTUAL FILE LOCATIONS:\n- TopToolbar.tsx: d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\TopToolbar.tsx (lines 78-95, 188-198)\n- LibraryView.tsx: d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\views\\LibraryView.tsx (lines 51-64, 102-156)\n- store.ts importFiles: d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\store\\store.ts (lines 342-363)\n- mediaFromFile/isAcceptedMediaFile: d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\lib\\media.ts (lines 127-172)\n- AIAnalyzer.tsx: d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\AIAnalyzer.tsx (lines 24-146)\n- modals.tsx (modal patterns): d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\modals.tsx (lines 1-389)\n- Modal.tsx (host): d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\Modal.tsx (lines 1-56)\n- classify.ts (source detection): d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\lib\\classify.ts (lines 1-78)\n- selectors.ts (albumById): d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\store\\selectors.ts (lines 16-18, 104-106)\n- CSS: d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\styles\\sdk.css (lines 975-1014)", "plan": "UPLOAD MODAL ARCHITECTURE:\n\n1. NEW FILE: UploadModal.tsx in packages/photo-sdk/src/components/\n - Component signature: function UploadModal({ defaultAlbumId?: AlbumId }): ReactNode\n - State: (files: File[], selectedAlbumId: AlbumId | null, uploading: boolean)\n - Three zones:\n a) Browse button (fileRef input with accept=\"image/*,video/*\")\n b) Drag-drop zone with visual feedback (dashed outline on hover)\n c) Album picker dropdown (defaulting to defaultAlbumId, showing user/folder albums)\n - File validation inline: filter out non-image/video before preview\n - Submit: passes (files, albumId?) to store.importFiles, then optionally addToAlbum\n - Uses apg-modal CSS (max-width 380px, centered via backdrop)\n\n2. MODIFIED: TopToolbar.tsx\n - Replace fileRef.current?.click() with openUploadModal() helper\n - Extract current album id from view state (view.startsWith('album:') ? view.slice(6) : null)\n - Pass defaultAlbumId to modal\n\n3. MODIFIED: LibraryView.tsx\n - Expose Welcome component's onImport callback to openUploadModal instead of fileRef.current?.click()\n - Keep drag-drop handler in LibraryView (it's a view-wide feature)\n\n4. NEW EXPORT: modals.tsx\n - Add openUploadModal(defaultAlbumId?: AlbumId) function\n - Follows pattern of openAlbumPickerModal, openSecuritySettings, etc.\n\n5. EXISTING FLOW (no changes needed):\n - store.ts importFiles: already validates via mediaFromFile → isAcceptedMediaFile\n - Already calls adapter.putBlob if available\n - Already prepends items to media array\n - store.addMedia: already auto-files by source (Screenshots album)\n - AIAnalyzer: already processes async in background (objects, faces, OCR, embedding)\n - Source classification: already happens in mediaFromFile via classify.ts\n - Screenshots auto-album: already in store.addMedia (line 329-338)\n\nINTEGRATION POINTS:\n- importFiles(files) signature stays same (no albumId param) — keep source-based auto-filing intact\n- IF user picks album in modal, call addToAlbum(albumId, newIds) AFTER importFiles completes\n- Processing is invisible (AIAnalyzer handles it; user sees aiStatus.running in toolbar)", "files": [ { "path": "d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\UploadModal.tsx", "change": "NEW FILE - Upload modal with browse button, drag-drop zone, and album picker. Validates file types inline, shows preview of selected files, lets user choose album before uploading. On submit, calls importFiles(files) then optionally addToAlbum(albumId, newIds). Exports helper openUploadModal() in modals.tsx." }, { "path": "d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\TopToolbar.tsx", "change": "MODIFY: Replace fileRef + onImportFiles with openUploadModal() call (line 189-195). Extract current albumId from view state to pass as defaultAlbumId. Remove fileRef, onImportFiles function. Update import to include openUploadModal from modals." }, { "path": "d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\views\\LibraryView.tsx", "change": "MODIFY: Replace Welcome onImport={() => fileRef.current?.click()} with onImport={() => openUploadModal()} (line 12, 69). Remove fileRef logic (lines 51, 54-58, 148-155). Keep drag-drop handler unchanged (lines 60-64, 102-156). Update import to include openUploadModal from modals." }, { "path": "d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\modals.tsx", "change": "MODIFY: Add export for openUploadModal(defaultAlbumId?: AlbumId) function at end of file. This opens the UploadModal via openModal() helper (following pattern of openSecuritySettings, etc.)." }, { "path": "d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\lib\\media.ts", "change": "NO CHANGE - isAcceptedMediaFile(file) already validates image/video MIME types and file extensions (line 127-129). mediaFromFile already handles both images and videos (line 132-172). Already used by importFiles." }, { "path": "d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\store\\store.ts", "change": "NO CHANGE - importFiles(files) already validates via mediaFromFile, uploads via adapter.putBlob if available, calls addMedia which auto-files by source (Screenshots album at line 328-338). Behavior unchanged." }, { "path": "d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\components\\AIAnalyzer.tsx", "change": "NO CHANGE - Already processes newly-added media in background: objects/objectLabels, faces (clustered into People), OCR text, embeddings. Triggered by mediaCount change. Runs with CONCURRENCY=2, updates aiStatus in toolbar (line 120, 115)." }, { "path": "d:\\advance-photo-gallery-web-sdk\\packages\\photo-sdk\\src\\lib\\classify.ts", "change": "NO CHANGE - classifyMediaSource already auto-detects Screenshots (line 40, 47, 52-54) and returns MediaSource. Screenshots album creation happens in store.addMedia (line 335-336)." } ] }, { "area": "Custom macOS-style Video Player for Lightbox", "findings": "\nCURRENT STATE:\n- Lightbox.tsx (line 147-148) renders videos using bare native `