Skip to content

fix: complete direct and capacitor parity for issue 399#412

Merged
hessius merged 38 commits intoversion/2.4.0from
fix/direct-capacitor-parity-399
May 1, 2026
Merged

fix: complete direct and capacitor parity for issue 399#412
hessius merged 38 commits intoversion/2.4.0from
fix/direct-capacitor-parity-399

Conversation

@hessius
Copy link
Copy Markdown
Owner

@hessius hessius commented Apr 28, 2026

Summary

  • reconciles issue audit: Horizontal review of DirectModeInterceptor feature parity #399 direct/Capacitor parity work on top of version/2.4.0 with upstream-first conflict handling
  • guards backend-only surfaces in direct/native modes and restores direct route contracts for profiles, history, recommendations, dial-in, pour-over, recipes, and profile sync behavior
  • fixes native machine URL persistence/resolution, onboarding discovery persistence, direct/native profile image handling, and unsupported direct variable override behavior

Verification

  • Backend: TEST_MODE=true .venv/bin/python -m pytest test_main.py -x -q (831 passed, 3 warnings)
  • Frontend: bun run test:run --reporter=dot (53 files, 980 passed, 1 skipped)
  • Frontend direct: bun run test:direct --reporter=dot (53 files, 980 passed, 1 skipped)
  • bun run lint
  • bun run build
  • bun run build:machine
  • bun run build:ios
  • Focused audit: Horizontal review of DirectModeInterceptor feature parity #399 suite: 14 files, 164 passed
  • Locale JSON parse: en/sv/de/es/fr/it ok
  • git diff --check

Fixes #399

hessius and others added 2 commits April 28, 2026 08:58
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread apps/web/src/services/shots/ShotDataServiceProvider.tsx Outdated
Comment thread apps/web/src/services/catalogue/CatalogueServiceProvider.tsx Outdated
Comment thread apps/web/src/services/machine/MachineServiceContext.tsx Outdated
hessius and others added 25 commits April 29, 2026 23:35
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>
hessius and others added 6 commits May 1, 2026 12:19
…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>
@hessius hessius requested review from Copilot May 1, 2026 18:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

hessius and others added 5 commits May 1, 2026 21:22
…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>
@hessius hessius merged commit 936ff59 into version/2.4.0 May 1, 2026
5 checks passed
@hessius hessius deleted the fix/direct-capacitor-parity-399 branch May 1, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants