Skip to content

v2.4.0: Dependencies, Settings Refactor, Accessibility & OEM Integration#356

Open
hessius wants to merge 155 commits intomainfrom
version/2.4.0
Open

v2.4.0: Dependencies, Settings Refactor, Accessibility & OEM Integration#356
hessius wants to merge 155 commits intomainfrom
version/2.4.0

Conversation

@hessius
Copy link
Copy Markdown
Owner

@hessius hessius commented Apr 7, 2026

v2.4.0 Release — Dependencies + OEM Integration + iOS App

Summary

This PR brings version/2.4.0 into main with:

  1. 17 Dependabot PRs absorbed — All dependency updates including 4 major bumps
  2. OEM TypeScript package integration (feat: integrate OEM TypeScript packages into DirectAdapter #326) — DirectAdapter uses espresso-api REST fallbacks for abort/purge/home; MeticAIAdapter uses apiFetch consistently
  3. Capacitor iOS app scaffolding (iOS app: native client via Capacitor + MachineDirectAdapter #253) — Full iOS project with native mode detection, URL resolution, and machine discovery

Dependency Updates

  • Backend: fastapi 0.135.3, uvicorn 0.44.0, python-multipart 0.0.24, sse-starlette 3.3.4, aiohttp 3.13.4
  • CI: codecov-action v5 → v6
  • Frontend major: lucide-react 0.577 → 1.7 (migrated all 30 private imports), i18next 25 → 26 + react-i18next 16 → 17, TypeScript 5.9 → 6.0
  • Frontend minor/patch: vite, react-hook-form, tailwind-merge, react-resizable-panels, happy-dom, plus 18 grouped updates

iOS App (#253)

  • Capacitor 8.x project (core, ios, camera, preferences)
  • Separated RuntimePlatform (web/machine-hosted/native) from MachineMode (direct/proxy)
  • Capacitor detection via window.Capacitor.isNativePlatform()
  • Native URL resolution: getServerUrl() returns machine URL in native mode (fixes all 314+ hook references)
  • DirectModeInterceptor prefixes relative /api/... URLs with machine base URL in native mode
  • Machine discovery service (mDNS/QR placeholders, working manual IP + connection tester)
  • CapacitorStorage adapter (wraps @capacitor/preferences, falls back to localStorage)
  • Reactive machine URL in MachineServiceContext
  • Info.plist: ATS local networking, Bonjour, camera, privacy manifest
  • Feature flags: CAPACITOR_FLAGS (machineDiscovery=true, pwaInstall=false)
  • 35+ new tests for machineMode, featureFlags, featureParity, discovery

OEM Integration (#326)

  • DirectAdapter.executeRawAction() for abort/purge/home via REST
  • MeticAIAdapter migrated from raw fetch to apiFetch, fixed response shapes and endpoint paths

Bug Fixes

  • Fixed CollapsibleSection closing tag in SettingsView (rebase conflict)
  • Fixed 3 race conditions in AppDatabase.ts (IDB transactions)
  • Fixed ChangeEvent imports in switch/slider/checkbox
  • Fixed SearchingLoader typo + dead meta field
  • Fixed ProxyAIService FormData field names
  • Fixed MeticAIAdapter endpoint paths and response parsing
  • Updated release notes to v2.4.0-beta.1

Test Results

  • Backend: 904 tests passing
  • Frontend: 585 tests passing (32 files), 0 lint errors
  • Build: Clean (TypeScript 6.0, Vite 8)

Exclusions

Closes #317, #326. Implements scaffolding for #253.

hessius and others added 30 commits March 25, 2026 15:11
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Expand MachineService interface: profiles, telemetry, history, settings
- Create ProxyAdapter (wraps MeticAI backend, Docker mode)
- Create DirectAdapter (uses @meticulous-home/espresso-api, PWA mode)
- Add MachineServiceProvider with mode selection (direct/proxy)
- Add machineMode utility (build-time + runtime detection)
- Install espresso-api, espresso-profile, @google/genai, idb, fzstd
- Install vite-plugin-pwa, fake-indexeddb (dev)
- Add VITE_MACHINE_MODE/VITE_DEFAULT_MACHINE_URL env type declarations
- All 320 tests pass, 0 lint errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create AIService interface (profile gen, shot analysis, image gen, recommendations, dial-in)
- Create ProxyAIService wrapping MeticAI backend endpoints
- Create BrowserAIService using @google/genai SDK directly
- Port prompt_builder.py to TypeScript (image, profile, analysis, recommendation, dial-in prompts)
- Create AIServiceProvider with mode selection (direct/proxy)
- All 320 tests pass, 0 lint errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create AppDatabase with idb library: settings, annotations, AI cache, pour-over, dial-in, profile images
- TTL-based AI cache (7-day expiry) with auto-cleanup
- LRU eviction for profile images (50 MB cap)
- Storage migration hook for first-run initialization
- All 320 tests pass, 0 lint errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create useMachineTelemetry hook supporting both proxy and direct modes
- Proxy mode: WebSocket to MeticAI backend /api/ws/live (existing pattern)
- Direct mode: Socket.IO via MachineService (espresso-api events)
- Field mapping from espresso-api StatusData to MachineState
- Exponential backoff reconnection, staleness detection
- All 320 tests pass, 0 lint errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create featureFlags module with proxy/direct mode flag sets
- Proxy mode: all features enabled (Docker backend)
- Direct mode: disable mDNS, scheduled shots, system mgmt, tailscale, MCP, cloud sync
- Direct mode: enable PWA install prompt, AI via browser SDK
- hasFeature() utility for conditional rendering
- All 320 tests pass, 0 lint errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add build:docker and build:machine scripts to package.json
- Add test:direct script for PWA-mode testing
- Configure vite-plugin-pwa with workbox caching strategies:
  - Static assets: CacheFirst
  - Machine API: NetworkFirst (5s timeout)
- PWA manifest with standalone display mode
- Machine build uses /meticai/ base path for Tornado static handler
- Manual chunk splitting: recharts, framer-motion, machine-api, genai
- Docker build: 5.8 MB output, Machine build: 5.8 MB output
- Both builds verified, all 320 tests pass, 0 lint errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- install-meticai.sh: resource checks, download, backup, extract
- validate-meticai.sh: verify files, routes, API connectivity
- update-meticai.sh: delegates to installer with backup
- uninstall-meticai.sh: interactive cleanup with confirmation
- All scripts include Tornado route configuration instructions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add build-machine-pwa.yml: builds VITE_MACHINE_MODE=direct, creates
  meticai-web.tar.gz artifact, runs lint and direct-mode tests
- Update auto-release.yml: build PWA tarball and attach to GitHub release
  with machine install instructions
- Update tests.yml: add test:direct step, include feature branch in CI
- All 320 tests pass in both proxy and direct modes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- machineMode.test.ts: 13 tests for mode detection, env vars, port detection
- featureFlags.test.ts: 13 tests for proxy/direct flags, hasFeature, caching
- AppDatabase.test.ts: 27 tests for IndexedDB CRUD, TTL cache, LRU eviction
- prompts.test.ts: 42 tests for all 6 prompt builders, tag system, safety

Total: 95 new tests (320 → 415), all passing in both modes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Critical wiring fixes:
- Wire AIServiceProvider into main.tsx component tree
- Replace useWebSocket with useMachineTelemetry in App.tsx
- Call useStorageMigration in App.tsx for IndexedDB init
- Gate Tailscale and Updates UI sections behind feature flags

Build fixes:
- Add maximumFileSizeToCacheInBytes to Workbox config (logo.png > 2MB)
- Exclude static manifest.json from Workbox precache glob
- Remove PNG from precache glob (large assets)

AI service improvements:
- Add wrapApiError() for user-friendly Gemini error messages (429/401/404)
- Wrap all generateContent/generateImages calls in try/catch
- Port full dial-in prompt with coffee params and iteration history

Other fixes:
- Map brew_head_temperature in direct mode telemetry
- Update LRU timestamp on profile image reads (true LRU semantics)
- Fix lint errors in test files (unused imports/vars)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- machineMode.ts: add comment clarifying meticulous.local fallback is
  proxy-mode only; direct mode uses window.location.host (same origin)
- install-meticai.sh: replace hardcoded meticulous.local with dynamic
  hostname detection; add note about randomized hostnames
- validate-meticai.sh: add CPU load average to system resource report

The Meticulous machine uses randomized hostnames (e.g.
meticulous-abc123.local), not a fixed meticulous.local. Since the PWA
is served from the machine itself, direct mode correctly uses
window.location.host. The install script now shows the actual hostname.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Meticulous machines don't have curl installed. All machine scripts now
use wget as primary HTTP tool with curl as fallback. Added fetch() and
download() helpers to install script, http_status() helper to validate
script. No new dependencies required — wget is standard on the machine.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… mode

The Meticulous machine has neither curl nor wget — only busybox and
python3. Updated HTTP helpers to try: busybox wget → python3 urllib →
curl → wget. Added --local flag for SCP-based installs where the
tarball is pre-copied to the machine. This is the recommended approach
for testing since no HTTP tool needs to be installed.

Usage: bash install-meticai.sh --local /tmp/meticai-web.tar.gz

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use TARBALL variable to reference the source directly instead of
copying to /tmp/meticai-web.tar.gz. Also preserves the user's
original file when using --local (only cleans up downloaded files).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The @meticulous-home/espresso-api package is CJS-only (exports.default).
Rolldown's production bundle wraps the default export differently than
dev mode, causing 'Object is not a constructor' at runtime. Added
interop that handles both cases: direct function or wrapped .default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inline env var in 'VAR=x cmd1 && cmd2' only applies to cmd1.
Using 'export VAR=x && cmd1 && cmd2' ensures vite build sees the
VITE_MACHINE_MODE=direct flag and applies the /meticai/ base path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Prefix logo, config.json, i18n loadPath with import.meta.env.BASE_URL
- Guard MeticAI proxy API calls (/api/settings, /api/history, /api/version)
  with isDirectMode() checks — these endpoints don't exist on the machine
- SettingsView: load/save settings from localStorage in direct mode
- Install script: skip backups for --local reinstalls, clean stale backups
- Recover ~12MB from accumulated backup directories on machine

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In direct mode, the Meticulous machine only has /api/v1/ endpoints.
MeticAI proxy endpoints (/api/settings, /api/machine/*, /api/history,
etc.) don't exist. Rather than guarding 80+ individual fetch calls,
install a global fetch interceptor in main.tsx that:

- Silently returns 404 for /api/<non-v1> paths (no network request)
- Passes through /api/v1/ paths to the Meticulous backend
- Passes through espresso-api calls (which use axios, not fetch)

Also skip config.json fetch entirely in direct mode (no file exists).

Verified on machine: all assets load, no 404 errors, machine API works,
storage stable at 2478 MB.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The fetch interceptor now translates key MeticAI proxy endpoints to
their Meticulous-native /api/v1/ equivalents:

- /api/machine/profiles → /api/v1/profile/list (wraps in {profiles})
- /api/machine/profile/:id/json → /api/v1/profile/get/:id
- /api/machine/status → synthetic idle response (real state via Socket.IO)
- /api/last-shot → /api/v1/history/last
- /api/history → /api/v1/history (wraps in {entries, total})
- All other proxy paths → 200 with empty JSON

Also guard profile image-proxy URLs (set via <img src>, bypasses
fetch interceptor) with isDirectMode() in ControlCenter,
ControlCenterExpanded, LiveShotView, and App.tsx.

Machine has 18 profiles that should now be visible through the
translated API layer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t mode interceptor

- Run profile: home → poll load (2s intervals, 10 retries) → start
- Run with overrides: same flow (overrides not supported in direct mode)
- Profile import from file: POST to /api/v1/profile/save
- Profile import from machine: no-op (already on machine)
- Import all: no-op success response
- Delete profile: translate to /api/v1/profile/delete/:id
- Machine commands: start/stop/load-profile → /api/v1/action/*
- jsonResponse helper now supports status codes

Verified on live machine: home→load takes ~4s, full run flow works.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three bugs fixed in direct mode interceptor:

1. Run profile no longer sends 'home' (which triggered a purge).
   Instead: try load → if busy, stop → retry with 2s backoff.

2. Preheat: POST /api/machine/preheat → GET /api/v1/action/preheat

3. Schedule shot: POST /api/machine/schedule-shot → preheat (if
   requested) + setTimeout for delayed profile load → start

Also added:
- /api/machine/profiles/orphaned → empty list (no MeticAI DB)
- /api/profiles/sync/status → zero counts
- POST /api/profiles/sync → no-op

Verified on live machine: no purge, preheat works, stop+load in ~2s.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Direct mode telemetry fixes:
- Convert profile_time from ms to seconds (shot timer was 1000x too high)
- Use data.profile/loaded_profile for active_profile (not data.name
  which is the stage name during brewing)
- Fetch target_weight from loaded profile's final_weight via API
- Map data.name to state field (shows stage like 'heating', 'preinfusion')

Profile catalogue:
- Add in_history/has_description fields to profile list response
- Wrap profile JSON in {profile: data} to match expected format

History:
- Translate machine history entries to MeticAI HistoryEntry format
  (id, created_at, profile_name, coffee_analysis, etc.)
- Convert epoch timestamp to ISO date string
- Same for /api/last-shot endpoint

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… shot analysis

- Fix profile load handler reading URL string as body instead of init.body
- Add retry logic for 409 'machine is busy' responses (stop + retry)
- Fix /api/shots/analyze interceptor for shot analysis in direct mode
- Expand API translation layer for machine-hosted PWA
- BrowserAIService uses Gemini SDK directly in browser for profile generation
- DirectAdapter, MachineService, MeticAIAdapter direct mode support
- Add profile validator and prompt for browser-based generation
- Hide Machine IP setting in direct mode, hide MQTT bridge in direct mode
- Profile catalogue navigates back to start view in direct mode
- Profile catalogue edit mode improvements for direct mode
- Telemetry unit conversion fixes (ms→s)
- ProfileBreakdown, RunShotView, PourOverView direct mode adjustments
- StartView profile catalogue navigation
- Add profileCatalogue.loaded, profileCatalogue.loadFailed keys
- Add pwa/direct mode related translation strings
Move padding-top from body to #root so the ambient background
gradient renders behind the notch while content stays below it.
hessius and others added 17 commits April 13, 2026 12:40
The dynamic `import('capacitor-zeroconf')` was hanging indefinitely in
the Capacitor WKWebView — Vite code-splits it into a separate chunk
that never loads. Confirmed by step-by-step diagnostics showing
execution stalls at STEP 2 (import).

Fix: use a static `import { ZeroConf } from 'capacitor-zeroconf'`
instead. The web fallback is safe (returns rejected promises, doesn't
crash), so there's no need for lazy-loading.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Capacitor 8 discovers plugins via NSClassFromString() from the
packageClassList in capacitor.config.json. With SPM static linking,
the linker strips ObjC classes that aren't directly referenced,
causing ALL SPM plugins (not just ZeroConf) to fail at runtime with
'plugin is not implemented on ios'.

The -ObjC flag forces the linker to keep all ObjC classes from static
libraries, fixing plugin registration for ZeroConf, Browser,
Preferences, Haptics, and all other SPM-linked plugins.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Capacitor 8 xcframework gates getString(_:), getBool(_:), reject()
and other APIs behind #if $NonescapableTypes (Swift 6.1 / Xcode 16.3).
Previously only Preferences and ZeroConf were patched; this extends
the patch script to automatically discover and fix ALL plugin Swift
files under @capacitor/, @capacitor-community/, and @aparajita/.

Newly patched: App, Browser, Clipboard, LocalNotifications, Share,
BiometricAuth, SecureStorage.

Also adds -ObjC to OTHER_LDFLAGS (both Debug and Release) so the
linker retains ObjC classes from SPM static libraries — required for
NSClassFromString() to find plugins at runtime.

The patch script now:
- Auto-discovers all plugin Swift files (no per-plugin config needed)
- Uses a robust Python parser for reject() calls (handles commas in
  string literals, nested parens, variable expressions)
- Adds -ObjC linker flag to pbxproj if missing
- Remains idempotent and safe to run multiple times

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extended patch-capacitor-spm.sh to handle ALL gated APIs discovered
during iterative build testing:
- getString/getBool/getInt/getFloat/getDouble with constant args (kKey)
- getArray with Type.self parameter
- UIColor.capacitor.color(fromHex:) → inline hex parser
- Data.capacitor.data(base64EncodedOrDataUrl:) → inline base64 parser
- JSTypes.coerceDictionaryToJSObject → as? JSObject cast
- bridge?.localURL(fromWebURL:) → NSObject perform selector
- bridge?.viewController → NSObject KVC cast

Verified: clean reinstall + patch + build succeeds on iPhone 16 Pro
simulator. All 13 Capacitor plugins compile correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The _meticulous._tcp mDNS service advertises port 80 (web UI), but the
API that we probe during connection testing is always on port 8080.
This caused auto-discovery to find the machine but fail the connection
test, preventing onboarding auto-configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ZeroConf.unwatch() and ZeroConf.close() never resolve their promises
on iOS, causing discoverMachines() to hang after finding the machine.
The discovery result was never returned to the onboarding wizard, so
auto-configure never triggered despite successful mDNS discovery.

Fix: fire-and-forget cleanup calls instead of awaiting them.
Also: use mDNS-reported port (port 80 works via nginx proxy),
and add diagnostic logging to auto-fill effect.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix Control Center not loading after onboarding: dispatch
  machine-url-changed event from setMachineUrl() so
  MachineServiceProvider reconnects Socket.IO to discovered IP

- Fix profile images not loading in Capacitor/direct mode:
  add useProfileImageSrc hook and resolveDisplayImage utility that
  resolve display.image from profile data (data URIs, CDN URLs,
  machine-relative paths) instead of relying on image-proxy which
  only works through fetch() interception, not <img src>

- Hide manual config in onboarding when auto-connected:
  show success state prominently, move discovery/manual sections
  behind a 'Configure manually' toggle

- Soften Buy Me a Coffee wording across all 6 locales:
  use 'consider' phrasing instead of imperative

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Node.js v22+ ships a built-in localStorage object that shadows
happy-dom's mock when --localstorage-file is not set. The methods
(setItem, getItem, clear) are undefined, causing all 27 tests to fail.

Fix: provide an in-memory localStorage shim via vi.stubGlobal().
Also add test for the new machine-url-changed event dispatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix Live View TDZ error (move machineState before hook call)
- Fix Pour Over 'can't find variable Pause' (add Pause import)
- Fix safe area insets (move padding to #root, fill edge-to-edge)
- Disable text selection/callout in iOS WKWebView
- Fix cancel preheat (use 'reset' instead of 'stop')
- Fix sounds toggle (optimistic status callback)
- Fix shot counter (handle history response format)
- Fix untranslated notifications (add i18n keys to 6 locales)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ction

- Use ref for i18n strings in useBrewNotifications to keep callback
  identity stable across language changes (prevents effect re-fire)
- Widen user-select exception to include pre, code, .selectable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move safe-area-inset-bottom from #root (scroll container) to content
wrapper via --safe-pb CSS variable. This ensures:
- No scroll when content fits viewport (collapsed control center)
- Proper bottom clearance when content overflows (expanded CC)
- App background fills the safe area zone (no black band)

Make #root a flex column so content wrapper uses flex-1 instead of
min-h-screen, preventing the content from always exceeding viewport.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Set contentInset: 'never' so CSS env() handles all safe area insets
  instead of native WKWebView adjustment conflicting with viewport-fit=cover
- Update backgroundColor to #030202 to match oklch(0.09 0.005 50) dark theme
- Disable native scrollView to prevent double-scroll with #root overflow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Network banner: add 10s polling safety net alongside event listener
for iOS where networkStatusChange events can be missed. Add cancelled
flag to prevent updates after unmount.

Temperature: clamp sensor values to 0-150°C range in both direct and
proxy telemetry paths. Transient glitches (e.g. 2000°C during state
transitions) are rejected and the previous valid reading is kept.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… tare

- abortShot now clears preheat_countdown via synthetic status callback,
  preventing the 'preheating' override from keeping UI stuck on 'heating'
- Purge action uses 'home' endpoint (the Meticulous API action that
  triggers a purge) instead of non-existent 'purge' endpoint
- Pour over weight card is now tappable to tare scale, matching live view

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Scroll #root to top in handleViewHistoryEntry so each profile detail
  view opens at the top instead of inheriting previous scroll position
- Resolve profile display.image via resolveDisplayImage() for
  direct/Capacitor mode before passing as cachedImageUrl to detail view

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove copy button (native share sheet has built-in copy)
- Remove three-dot context menu (duplicated tab bar + share)
- Remove inline 'Export as Image' button in Analyze tab
- Replace with enhanced share button showing action sheet:
  Share Summary (text), Share Chart (PNG), Share Analysis (PNG)
- Chart and analysis use domToPng → shareImageDataUri pipeline
- Add shareImageDataUri() to useNativeShare for image sharing
  via data URI on native and Web Share API files on web
- Fix SPM patch regex to handle dotted constant args
  (e.g. Constants.MethodParameter.path)
- Add i18n keys for all 6 locales

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hessius hessius requested a review from Copilot April 13, 2026 21:45
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
Copy link
Copy Markdown
Owner Author

hessius commented Apr 13, 2026

Addressing 2nd round of Copilot review comments (Apr 10)

All 4 unresolved comments about API response parsing have been fixed in prior commits:

MeticAIAdapter.ts:

  • listProfiles() — parses { profiles: Profile[] } wrapper, maps to ProfileIdent (lines 99-101)
  • fetchAllProfiles() — fetches listing for IDs, then individually fetches via /api/machine/profile/{id}/json and unwraps resp.profile (lines 103-117)
  • getProfile() — types response as { profile: Profile }, returns resp.profile (lines 119-122)

ProxyCatalogueService.ts:

  • getProfileJson() — types response as { profile: Profile }, returns resp.profile (lines 67-72)

All endpoints now correctly unwrap the backend's response wrapper objects.

hessius and others added 10 commits April 14, 2026 00:01
…rors

- Fix useProfileImageCache tests failing in direct mode by mocking
  machineMode to proxy (tests verify caching logic, not mode-specific
  image resolution)
- Add DirectAdapter unit tests (44 tests): connection lifecycle, socket
  events, brewing commands, abortShot/purge/tare, sounds, settings,
  history, profiles, subscribe/unsubscribe patterns
- Add useNativeShare tests (12 tests): native/web/fallback share paths,
  shareImageDataUri, AbortError handling, download fallback
- Fix TypeScript: StatusData cast for synthetic status fields, definite
  assignment for resolveWatch, HistoryListingEntry cast, stale Storybook
  props

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hine info

- Reorder sections: Profile → Temperatures → Actions → Settings → Machine Info
- Fix telemetry seed race condition: store seeded values and replay on
  first subscriber so total_shots, firmware_version, and sounds_enabled
  always reach the UI regardless of subscription timing
- Seed firmware_version from getDeviceInfo() during connect
- Fix profile selection crash: null guard on p.profile?.name
- Enrich Machine Info: fetch DeviceInfo on expanded mount, show serial,
  software version, image version, update channel, model version
- Hide individual info rows when null; hide entire section if no data
- Larger profile image (10×10) in collapsed view with change icon
- Add profile selector to collapsed view via ArrowsClockwise button
- Add CaretDown icon to native profile selector button
- Filter profiles with missing names from selector
- Add i18n keys for new machine info labels in all 6 locales

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dling

- Fix loadProfile() to handle both flat {name,id} and nested {profile:{name,id}} response shapes from machine API
- Add Cancel button (ActionSheetButtonStyle.Cancel) to native ActionSheet
- Change profile change icon from ArrowsClockwise to CaretUpDown

Closes part of #253

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ix grouphead temp

- Remove active profile section from expanded view (now in collapsed view)
- Reorder sections: Actions → Temperatures → Settings → Machine Info
- Map sensors.g to brew_head_temperature (was incorrectly using sensors.t for both)
- Show grouphead temp only when it differs from boiler temp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
sensors.g is 0 (not undefined) when the grouphead sensor isn't
reporting — use || instead of ?? so 0 falls back to sensors.t.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ouphead sensor

sensors.g flickers between 0 and real values causing UI jitter.
Use sensors.t (boiler) for both temperatures until grouphead stabilises.
Compact control centre now shows boiler_temperature directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The machine sends brew_head_temperature via the Socket.IO 'sensors'
event (Temperatures.t_ext_1), NOT via sensors.g in the 'status' event.
sensors.g is unreliable and flickers between 0 and real values.

- Add onTemperatures to MachineService interface + all adapters
- Subscribe to 'sensors' Socket.IO event in DirectAdapter
- Map t_ext_1 to brew_head_temperature in useMachineTelemetry
- Update adapter parity tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove discovery debug log from onboarding machine step
- Move IP hint text to the configure manually section
- Add safe-area-inset-top to no-internet banner (dynamic island)
- Add 5s polling on web for network status (events can be missed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add 'Get a free API key from Google AI Studio' button that opens
  aistudio.google.com/app/apikey (Capacitor Browser on native, window.open on web)
- Fix TS7030: explicit return undefined in auto-fill useEffect
- Fix TS2367: extract isTesting before JSX narrowing in manual config block
- Remove leftover console.error debug logs from auto-fill
- Add onboarding.ai.getKey i18n key to all 6 locales

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v2.4 Release Coordination — Three-Pronged Deployment

2 participants