fix: complete direct and capacitor parity for issue 399#412
Merged
hessius merged 38 commits intoversion/2.4.0from May 1, 2026
Merged
fix: complete direct and capacitor parity for issue 399#412hessius merged 38 commits intoversion/2.4.0from
hessius merged 38 commits intoversion/2.4.0from
Conversation
Preserve the recovered local issue #399 implementation before rebasing onto the human-tested version/2.4.0 branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR closes the remaining feature-parity gaps between proxy (Docker), direct PWA, and Capacitor native modes for issue #399 by centralizing mode capability decisions, improving native/direct machine URL persistence & resolution, and replacing “stub” behavior with IndexedDB-backed implementations for direct mode.
Changes:
- Add a three-mode capability matrix (
proxy/directPwa/capacitor) and enforce it via parity tests; guard backend-only UI/actions behind feature flags. - Introduce Capacitor-aware persistence for machine URL (Preferences vs localStorage) plus a shared
useResolvedMachineUrl()hook. - Implement direct-mode storage adapters (annotations, dial-in sessions, pour-over prefs, profile images) and add a direct PWA service worker + install prompt.
Reviewed changes
Copilot reviewed 62 out of 64 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/web/src/vite-end.d.ts | Extend VITE_MACHINE_MODE typing to include capacitor. |
| apps/web/src/services/storage/index.ts | Export the new capacitorStorage adapter. |
| apps/web/src/services/storage/CapacitorStorage.ts | Add Preferences-backed storage with localStorage fallback. |
| apps/web/src/services/storage/CapacitorStorage.test.ts | Unit tests for web vs native storage selection. |
| apps/web/src/services/storage/AppDatabase.ts | Expand IndexedDB schema to support direct-mode dial-in + image storage; add delete helpers and safer merges. |
| apps/web/src/services/shots/ShotDataServiceProvider.tsx | Use resolved machine URL for direct mode service creation. |
| apps/web/src/services/machine/useResolvedMachineUrl.ts | New hook to async-resolve machine URL and react to changes. |
| apps/web/src/services/machine/machineUrl.ts | Centralize machine URL fallback, resolution, and persistence using capacitor-aware storage. |
| apps/web/src/services/machine/machineUrl.test.ts | Tests for persistence and native vs web fallback behavior. |
| apps/web/src/services/machine/MachineServiceContext.tsx | Switch to resolved URL + exported MACHINE_URL_CHANGED. |
| apps/web/src/services/machine/MachineServiceContext.test.tsx | Ensure native URL resolution is awaited before connecting. |
| apps/web/src/services/interceptor/directModeStorage.ts | Add IndexedDB-backed direct-mode implementations (annotations, history notes, pour-over prefs, dial-in sessions, images). |
| apps/web/src/services/interceptor/directModeStorage.test.ts | Coverage for direct-mode storage adapters + migrations/validation. |
| apps/web/src/services/interceptor/directModeHttp.ts | Add helpers for request parsing + proxy-path detection. |
| apps/web/src/services/interceptor/directModeHttp.test.ts | Tests for JSON responses, request parsing, and API-path classification. |
| apps/web/src/services/catalogue/CatalogueServiceProvider.tsx | Use resolved machine URL for direct mode service creation. |
| apps/web/src/pwaServiceWorker.test.ts | Validate service worker activation only clears intended caches. |
| apps/web/src/lib/featureParity.test.ts | Expand parity tests to include Capacitor and enforce intentional per-mode differences. |
| apps/web/src/lib/featureFlags.ts | Export MODE_CAPABILITY_MATRIX; harden hasFeature() default. |
| apps/web/src/lib/featureFlags.test.ts | Add tests for the capability matrix and hasFeature() edge cases. |
| apps/web/src/lib/constants.ts | Add a new direct-mode analysis cache key. |
| apps/web/src/lib/config.test.ts | Assert warning logging when config fetch fails. |
| apps/web/src/hooks/useUpdateTrigger.ts | Guard update triggering behind watchtowerUpdate feature flag. |
| apps/web/src/hooks/useUpdateTrigger.test.ts | Tests for update triggering guards + error handling. |
| apps/web/src/hooks/useUpdateStatus.ts | Guard periodic/manual update checks behind watchtowerUpdate. |
| apps/web/src/hooks/useUpdateStatus.test.ts | Add mocks for config + feature flags. |
| apps/web/src/hooks/useUpdateStatus.direct.test.ts | Direct-mode tests ensuring update calls do not fire when disabled. |
| apps/web/src/hooks/usePwaInstall.ts | Add PWA install hook + SW registration gated by pwaInstall. |
| apps/web/src/hooks/useProfileImageSrc.ts | Resolve profile images using the resolved machine URL (async) and handle both profile.image and profile.display.image. |
| apps/web/src/hooks/useProfileImageSrc.direct.test.ts | Direct/native tests for async image URL resolution. |
| apps/web/src/hooks/useProfileImageCache.ts | Use async image resolution for direct/native (no interceptor for <img>). |
| apps/web/src/components/SettingsView.tsx | Add machine URL normalization/persistence, guard backend-only surfaces by feature flags, and improve native discovery persistence. |
| apps/web/src/components/SettingsView.direct.test.tsx | Tests ensuring backend calls are skipped in direct mode and URL persistence works in web/native. |
| apps/web/src/components/RunShotView.tsx | Guard scheduling flows behind scheduledShots and refactor some decision logic into helpers. |
| apps/web/src/components/RunShotView.helpers.ts | Extract scheduling/visibility helper functions. |
| apps/web/src/components/RunShotView.direct.test.tsx | Tests for scheduled-shots guards and helper behavior. |
| apps/web/src/components/PwaInstallPrompt.tsx | Add UI prompt for PWA install, using usePwaInstall(). |
| apps/web/src/components/PwaInstallPrompt.test.tsx | Tests covering SW registration + prompt flow. |
| apps/web/src/components/ProfileSync.direct.test.tsx | Tests ensuring sync controls/backend calls are skipped when cloudSync is disabled. |
| apps/web/src/components/ProfileRecommendations.tsx | Use async machine URL image resolution in direct/native mode. |
| apps/web/src/components/ProfileDropdown.tsx | Include image field in dropdown profile type. |
| apps/web/src/components/ProfileCatalogueView.tsx | Guard cloud sync UI/calls by feature flag; improve direct/native image URL handling. |
| apps/web/src/components/OnboardingWizard.tsx | Persist machine URL through shared persistence and prevent stale async state updates. |
| apps/web/src/components/OnboardingWizard.direct.test.tsx | Tests for native Preferences persistence during onboarding. |
| apps/web/src/components/HistoryView.tsx | Guard sync badge calls by cloudSync; use async direct/native image resolution. |
| apps/web/src/components/FindSimilarOverlay.tsx | Use async direct/native image resolution. |
| apps/web/src/components/ControlCenter.tsx | Resolve machine-relative images using the resolved machine URL in direct/native mode. |
| apps/web/src/App.tsx | Guard cloud sync application and use async image URL resolution in direct/native flows. |
| apps/web/src/App.direct.test.tsx | Tests that cloud sync polling/settings application is skipped when disabled. |
| apps/web/public/sw.js | Add direct-PWA service worker with scoped caching and cache cleanup. |
| apps/web/public/manifest.json | Add larger icon entry. |
| apps/web/public/locales/en/translation.json | Add PWA install strings; add scheduling/unavailable + machine URL error strings. |
| apps/web/public/locales/sv/translation.json | Add scheduling/unavailable + machine URL error strings. |
| apps/web/public/locales/de/translation.json | Add scheduling/unavailable + machine URL error strings. |
| apps/web/public/locales/es/translation.json | Add scheduling/unavailable + machine URL error strings. |
| apps/web/public/locales/fr/translation.json | Add scheduling/unavailable + machine URL error strings. |
| apps/web/public/locales/it/translation.json | Add scheduling/unavailable + machine URL error strings. |
| apps/web/ios/App/App/MeticulousViewController.swift | Register local Capacitor plugin via KVC/ObjC dispatch for Capacitor 8 SPM + Xcode compatibility. |
| apps/web/ios/App/App/MeticulousDiscoveryPlugin.swift | Add native mDNS discovery plugin for Capacitor. |
| apps/web/ios/App/App/Base.lproj/Main.storyboard | Swap root controller to MeticulousViewController. |
| apps/web/ios/App/App.xcodeproj/project.pbxproj | Add the new Swift source files to the Xcode project build. |
| apps/server/test_main.py | Update recipe name expectations (4:6 naming). |
Replace null initialization in useResolvedMachineUrl with synchronous getMachineUrlFallback() so providers always have a URL to render with. This prevents MachineServiceProvider, ShotDataServiceProvider, and CatalogueServiceProvider from returning null (blank screen) while the async URL resolution completes in direct/native mode. The fallback URL (meticulous.local:8080) does not read from localStorage, preserving the stale-URL avoidance behavior. The async resolution still runs and updates to the correct URL within milliseconds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace KVC value(forKey:) with class_copyIvarList/object_getIvar to access the private capacitorBridge property on CAPBridgeViewController. KVC throws NSUnknownKeyException for private Swift properties in the Capacitor 8 xcframework, causing an immediate crash on launch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n flow Revert to the base branch's proven sync setMachineUrl (localStorage) approach for immediate URL availability, while also persisting to Capacitor Preferences in the background for durability. The async-only persistMachineUrl blocked the connection success state behind an await that could fail silently on iOS. Also fix machine name text overflow in discovery results by adding whitespace-normal and break-all to the machine button. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…first save The sync-first save approach now writes to both localStorage (immediate) and Capacitor Preferences (background), so the test should verify localStorage contains the URL rather than expecting null. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On native iOS, resolveMachineUrl() only checked Capacitor Preferences. When the async Preferences write hasn't completed yet (race condition during onboarding), it fell back to meticulous.local:8080 instead of the actual machine IP that was already written to localStorage by setMachineUrl(). Add localStorage as a fallback source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The localStorage fallback in resolveMachineUrl() should only apply on native platforms where the race between sync localStorage write and async Capacitor Preferences write causes the connection issue. On web, getDefaultMachineUrl() already handles localStorage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rewrite useResolvedMachineUrl to match the base branch's proven approach: initialize from getDefaultMachineUrl() (sync localStorage) and read localStorage directly in the event handler. The async-only Capacitor Preferences path introduced timing issues on native iOS that prevented post-onboarding machine connection. The async resolveMachineUrl() is still called on mount as a secondary check for URLs persisted only in Capacitor Preferences. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The DirectModeInterceptor refactor dropped several critical features: - derived_tags field in _processProfileList() (breaks catalogue filtering) - /api/settings GET/POST (breaks settings view in direct mode) - /api/health, /api/version, /api/network-ip endpoints - /api/machine/detect stub - /api/profile/:id/regenerate-description handler - Admin route stubs (update-method, tailscale-status, changelog, etc.) Restore all missing handlers from the base branch implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
canShowVariableAdjustments was incorrectly gated on scheduledShotsEnabled, hiding the variable adjustment panel in direct/PWA and Capacitor modes where scheduledShots is always false. Variable adjustments are unrelated to scheduling — they let users tweak profile variables before running. The base branch (version/2.4.0) had two render paths (left column for scheduledShots=true, right column for false). The refactor into a helper accidentally required scheduledShotsEnabled=true for both paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The async resolveMachineUrl() in _fetch caused profile loading failures
across Profile Catalogue, Control Center, and Shot Analysis. Errors were
silently masked as empty results ({ profiles: [] }).
Changes:
- Revert _fetch to sync getDefaultMachineUrl() (proven base branch approach)
- Remove _nativeMachineApiUrl async helper
- Restore ONBOARDING_COMPLETE guard on background prefetch
- Revert /api/machine/profiles handler to direct _fetch pattern
- Remove diagnostics endpoint and debug logging
- Clean up unused resolveMachineUrl import
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…verride route - Extend _fetch to handle URL objects and Request inputs (not just strings) - Replace async resolveMachineUrl() with sync getDefaultMachineUrl() in image proxy - Revert run-profile-with-overrides to return 501 (not supported in direct mode) - Use _originalFetch for image proxy (image paths are not /api/ prefixed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ution - App.tsx: revert resolveDisplayImageAsync → resolveDisplayImage (sync) in both handleViewMachineProfile and handleViewProfileByName to prevent navigation blocking when Capacitor Preferences hangs on native - ControlCenter.tsx: remove resolvedMachineUrl truthiness guard that prevented image resolution when URL is empty string — resolveDisplayImage already has its own fallback via getDefaultMachineUrl() - DirectModeInterceptor.ts: use resolveMachineUrl() (async, reads real localStorage) instead of getDefaultMachineUrl() (mocked in tests) for image proxy handler — fixes 3 CI test failures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- DirectModeInterceptor: fetch full profile (with stages) from machine
via /api/v1/profile/get/{id} before editing — the profile list cache
doesn't include stages, so the machine rejected saves with 400
- DirectModeInterceptor: remove machine history validation from notes
handler — notes are local-only (IndexedDB), don't need machine round-trip
to verify entry exists. Base branch also saves without validation.
- useProfileImageSrc: use sync resolveDisplayImage instead of async
resolveDisplayImageAsync to prevent potential hangs on native
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The hook now uses sync resolveDisplayImage (via getDefaultMachineUrl) instead of async resolveDisplayImageAsync (via resolveMachineUrl). Update test expectation to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When fetching full profile via /api/v1/profile/get/{id}, validate that
the response body actually contains a profile (has id field) before
using it. Falls back to cached profile from the list on invalid response.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix image upload: fetch full profile (with stages) before saving to machine, same pattern as profile edit fix - Strengthen no-text directive in image generation prompts (both frontend and backend) to explicitly forbid labels, watermarks, signatures, and typography - Regenerate static description after profile edit so it reflects the changes immediately - Update profile breakdown after save by using the returned profile data directly instead of re-fetching from history (which doesn't find machine profiles) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n, persistence) - Replace all resolveDisplayImageAsync with sync resolveDisplayImage (4 locations) Fixes: Find Similar images, profile detail image, recommendations, image cache - Fix generated image save: use data URI directly on native instead of proxy URL - Fix export as image: use shareImageDataUri on native (WKWebView can't open data: URLs) - Fix export as JSON: use Capacitor Filesystem + Share API on native (blob: URLs fail) - Fix notes persistence: fetch saved notes when creating synthetic profile entries - Fix AI description persistence: save to _descriptionCache in interceptor + update entry - Fix swipe back: add profile-catalogue case to handleSwipeRight switch - Fix edit button alignment: reduce margin above ProfileBreakdown - Fix app loading: never block render on native (isInitializing starts false) - Fix profile detail delay: navigate immediately with cached data, fetch full profile in bg - Add @capacitor/filesystem dependency for native file exports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Update useProfileImageSrc.direct.test.ts: expect sync URL (stale-localstorage) since useProfileImageCache now uses sync resolveDisplayImage - Fix 62 unused imports (F401) across all Python server files - Fix 23 unused variables/f-strings (F541, F841, E712) in server - Format all Python files with ruff (44 files) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The import was removed by ruff as 'unused' but it's accessed by tests via main._scheduled_shots. Added noqa comment to prevent future removal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- _saveProfileImageData: don't embed large data URI in display.image
when saving to machine — store only in local IndexedDB cache.
AI-generated images can be several MB, causing the machine's
/api/v1/profile/save to reject the oversized payload.
- shareImageDataUri: write image to temp file via @capacitor/filesystem
before sharing — Share.share({ url: dataUri }) shares the raw base64
text instead of the image on iOS.
- Update useNativeShare.test.ts to match new temp-file sharing behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tests no longer assert that profile images are embedded in the machine profile's display.image field — images are now stored only in local IndexedDB to avoid oversized payloads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TypeScript fixes (0 errors, was 90):
- ControlCenterExpanded: extend DeviceInfo with Record for model_version
- DemoCatalogueService: fix Display property casing (camelCase)
- DirectCatalogueService/DirectAdapter: safe ProfileIdent cast
- DirectModeInterceptor: Promise.resolve wrap, MACHINE_IP→MACHINE_URL
- DemoAdapter: add missing Settings props, fix DeviceInfo, remove rateShot
- demoProfiles: convert dynamics from {type,value} to {points,over,interpolation}
- chart.tsx: @ts-nocheck for vendored shadcn/ui recharts types
- TasteCompassInput.test: type vi.fn() mocks properly
- VariableAdjustPanel.test: type vi.fn() mocks properly
- BrowserAIService: fix Error.cause for ES2020 target
- PourOverView: .at(-1) → .slice(-1)[0]
- ProfileCatalogueView: add explicit type annotations
- Previous batch: MachineService, LlmAnalysisModal, FeatureErrorBoundary,
ErrorFallback, validationUtils, useBiometrics, machineMode, ControlCenter
Bug fixes:
- Generated profile images: compress + save to machine via IndexedDB
- AI image safety: no people/faces/portraits, must be artistic
- Image buttons: flex-wrap to prevent overflow on small screens
- Edit button alignment: wrap with ProfileBreakdown container
- Profile creation: only show Coffee Analysis card when image provided
- FindSimilarOverlay: use image-proxy URL for thumbnails
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace @ts-nocheck with proper 'any' typing for vendored shadcn/ui recharts component (ChartTooltipContent, ChartLegendContent) - Add ios/DerivedData to eslint ignores (Capacitor build artifacts) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The _compressImageForMachine helper uses new Image() + canvas which doesn't fire onload in jsdom test environments. Add a 3s timeout so the compression fails fast and falls back to keeping the original display.image value instead of hanging until the test timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…oji prefixes - ProfileBreakdown: add headerAction prop to render edit button inline with title - HistoryView: pass edit button via headerAction instead of separate div - DirectModeInterceptor: cache AI description only after successful save - DirectModeInterceptor: convert info/information variables to info_ prefixed keys with emoji names, preserving semantic types where valid - Use Extended_Pictographic regex for accurate emoji detection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- RunShotView: replace Coffee icon placeholder with ProfileImage component - RunShotView: add profile images to selector dropdown items - RunShotView: fetch profile images via useProfileImageCache on mount - DirectModeInterceptor: implement run-profile-with-overrides handler using POST /api/v1/profile/load for ephemeral loading - Support save_original, save_new, and none save modes - Apply variable overrides client-side with top-level field sync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ption hydration
- Fix notes not persisting after save by calling onEntryUpdated with updated notes
- Compress large AI-generated images (>500KB) to 1024px JPEG before saving
- Fix native export using Share.share({ files }) instead of url (Capacitor Share API)
- Fix image-proxy using sync getDefaultMachineUrl instead of async resolveMachineUrl
- Merge description cache into history API responses for persistence across navigation
- Remove unused resolveMachineUrl import from DirectModeInterceptor
- Update useNativeShare test to match new files-based sharing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The image-proxy handler now uses getDefaultMachineUrl() (sync) instead of resolveMachineUrl() (async). Tests must mock getDefaultMachineUrl via the hoisted machineModeMocks rather than setting localStorage directly, since getDefaultMachineUrl is module-mocked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… exits live view - Fix AI shot analysis returning fabricated results: interceptor now fetches actual shot telemetry data, computes per-stage metrics, and builds a comprehensive prompt matching the server's format before calling Gemini - Add shot-aligned target curves to analysis response using actual stage timings from the shot history (not estimated durations) - Abort during warm-up/shot now exits the live view immediately - Guard against empty telemetry data in LLM analysis handler - Use profile_time consistently for time calculations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ive views - Stop shot now exits live view (calls onBack like abort does) - Fix iOS double share sheet by not passing text with files - Prevent long-press selection on all chart containers - Fix checkbox alignment in recommendation selection dialog - Fix apply recommendations in direct mode (fetch full profile) - Derive stage ranges from target curves when shot data lacks them - Add dedicated Export as Image button on analysis tab - Polish ExpertAnalysisView: consolidate action buttons, tighten spacing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- fix(i18n): correct onboarding text to 'The all-in-one toolkit' - fix(onboarding): persist API key to SecureStorage (Keychain) on native - fix(secure-storage): fall back to localStorage when Keychain returns null - fix(settings): add 5s loading timeout to prevent infinite spinner - fix(home): hide support banner on two-column layout - fix(home): hide QR code button on Capacitor (native) platform - feat(home): add Settings button to StartView on desktop layout - fix(greeting): skip 'just brewed' Dynamic Island for pour-over profiles - fix(pour-over): restore previous profile after cleanup in direct mode - chore: bump version to 2.4.0-beta.2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract analysisResult?.profile_target_curves to a local variable so the manual useMemo dependencies match the compiler's inferred deps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ddition - E2E: use .first() for Settings button locators (desktop has two buttons) - Unit: pass text option through to Share.share on native image share - Lint: extract profileTargetCurves variable for React Compiler Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Verification
Fixes #399