Skip to content

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

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

v2.4.0: Dependencies, Settings Refactor, Accessibility & OEM Integration#356
hessius wants to merge 90 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 20 commits March 27, 2026 20:55
- Checkbox: forward id, aria-*, data-* props to Konsta component
- Switch: forward id, name, aria-*, data-*, defaultChecked to Konsta Toggle
- Slider: map onInput→onValueChange, onChange→onValueCommit; forward aria-*
- useKonstaOverride: shared subscriber set (single global listener pair)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix same-tab reactivity: dispatch custom event from useKonstaToggle
  so useKonstaOverride re-renders without page refresh
- Add clarifying comment for 'none' → 'material' theme mapping
- Add unit tests for useKonstaOverride and useKonstaToggle hooks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: extract fetch interceptor from main.tsx to DirectModeInterceptor

Move ~1350 lines of direct-mode fetch interception logic from main.tsx
to services/interceptor/DirectModeInterceptor.ts. main.tsx now calls
installDirectModeInterceptor() behind the isDirectMode() guard.

- Extract all route handlers, profile cache, and prefetch logic
- Move jsonResponse helper and CachedProfile interface
- Change catch-all from silent {} to 501 Not Implemented with console.warn
- Zero behavioral change for all handled routes

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

* fix: address code review findings for DirectModeInterceptor

- Use STORAGE_KEYS constants instead of hardcoded localStorage strings
- Wrap JSON.parse of cached data in try/catch to handle corruption
- Fix pour-over handlers: read body from Request object, not URL string
- Update main.tsx comment to reflect 501 behavior for unhandled routes

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

---------

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

feat(konsta): foundation — KonstaProvider, hooks, theme CSS removal
Keep shared subscriber pattern from review fix, resolve extra brace from merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
feat(konsta): adaptive UI components — Konsta rendering on mobile
)

- useKonstaOverride() always returns false (original logic preserved)
- Platform theme dropdown and Konsta toggle hidden in Settings
- CSS scoping rules preserved but dormant
- All adaptive components remain intact for future re-enablement
- Backend: catch MachineUnreachableError in list_machine_profiles, return
  history-based profiles with offline flag as fallback
- Frontend: add Import button and ProfileImportDialog to ProfileCatalogueView
- Frontend: show offline banner when machine is unreachable
- Frontend: defensive Array.isArray guard on profile data
- i18n: add offlineBanner and importButton keys to all 6 locales
- UI: adjust StartView button heights and App layout for mobile
- Compact single-line buttons replacing overflow-prone two-line layout
- Distinct icons: Desktop (machine), Archive (MeticAI), Trash (everywhere)
- Clear labels: 'Delete from machine' / 'Remove from MeticAI only' / 'Delete everywhere'
- Visual hierarchy: outline buttons for single targets, destructive for delete everywhere
- Updated all 6 locale files (en/sv/es/fr/de/it)
- Fixed restore-to-machine endpoint: bypass Pydantic model validation,
  inject author_id, strip None values, use httpx direct POST
- Removed bg-background from ProfileCatalogueView wrapper
…tivity

- Replace Pause icon with Stop icon on the stop button
- Hide bloom weight target display and settings in free mode
  (dose is unavailable so the feature doesn't work)
- Increase auto-start confirmation: 800ms→1500ms, 3g→8g escape hatch
  to prevent false triggers when pouring in grounds
…nges

Profiles generated or imported by MeticAI were never getting a
content_hash stored in their history entry. The sync endpoints rely on
comparing stored_hash vs machine_hash to detect changes — with no
stored hash, the comparison was silently skipped and modifications made
in the official Meticulous app were invisible.

Root cause chain:
- save_to_history() created entries without content_hash
- import_profile() same issue
- sync_profiles() short-circuited: `if stored_hash and ...`
- auto_sync_profiles() explicitly skipped: `if not stored_hash: continue`
- sync_status() always returned updated_count=0

Fixes:
- save_to_history: compute content_hash from profile_json
- import_profile: compute content_hash from profile dict
- coffee.py: fetch-back profile from machine after upload to store
  the machine-consistent hash (avoids false mismatches from field
  normalization)
- sync_profiles + auto_sync: backfill missing hashes from machine
  without flagging as 'updated' (first encounter = baseline)
- sync_status: perform full profile fetch + hash comparison so badge
  count accurately reflects pending updates

Tests: 4 new tests (904 total), all passing.

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

Rewrote 3 of 5 golden rules (ruleStartSimple, ruleYieldTime,
rulePreciseControl) across all 6 locales (en, de, es, fr, it, sv):

- Rule 1: Removed contradiction — now frames advanced control as
  complementary ("works best when basics are dialed") instead of
  discouraging it
- Rule 3: Added flavor direction guidance — profile adjustments are
  more effective than grind alone
- Rule 5: Replaced vague "use precise control" with specific flavor
  steering guidance (flow→acidity/body, temperature ranges by roast
  level, pressure tapering)

Kept ruleDontChangeDose and ruleEvenExtraction unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create reusable CollapsibleSection component with framer-motion animation
- Reorganize settings into logical groups:
  - Configuration: Language, IP, AI Settings, Author, MQTT, Appearance,
    Tailscale, and Beta Testing all in one card with collapsible sections
  - Version & Changelog: combined card with version info, changelog, updates
  - System: danger zone kept as-is at bottom
- Replace Save button with debounced auto-save (800ms) + subtle checkmark
- Add version subtitle to header
- Add i18n keys: aiSettings, versionAndChangelog, mqttBridge (all 6 locales)
- Remove deprecated saveSettings/saving i18n keys
- Remove FloppyDisk icon import and isSaving/saveStatus state

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Change desktop aside from hidden lg:block to hidden md:block so the
desktop control center renders at >=768px, matching the isMobile cutoff.
Previously, neither mobile nor desktop control center rendered in the
768-1023px range.

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

- Extend featureFlags.test.ts with snapshot tests and exhaustive per-flag checks
- New featureParity.test.ts: structural parity, shared/proxy-only/direct-only
  feature classification, completeness guard
- New MachineService.test.ts: adapter interface parity (29 methods), telemetry
  subscription contracts, AIService adapter parity

127 new tests (550 total frontend). Any new flag or adapter method added to
one mode but not the other will fail immediately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Backend:
- fastapi 0.135.2 → 0.135.3
- uvicorn 0.42.0 → 0.44.0
- python-multipart 0.0.22 → 0.0.24
- sse-starlette 3.3.3 → 3.3.4

Bridge:
- aiohttp 3.13.3 → 3.13.4

CI:
- codecov/codecov-action v5 → v6

Frontend (minor/patch):
- vite 8.0.2 → 8.0.5
- react-hook-form 7.70.0 → 7.72.0
- tailwind-merge 3.4.0 → 3.5.0
- react-resizable-panels 4.7.6 → 4.8.0
- happy-dom 20.8.8 → 20.8.9
- i18next-http-backend 3.0.2 → 3.0.4
- marked 17.0.4 → 17.0.6
- react-easy-crop 5.0.8 → 5.5.7
- @chromatic-com/storybook 5.0.2 → 5.1.1
- @playwright/test 1.58.2 → 1.59.1
- @storybook/* 10.2.19 → 10.3.4
- @vitest/browser-playwright 4.1.0 → 4.1.2
- @vitest/coverage-v8 4.1.0 → 4.1.2
- axe-core 4.11.1 → 4.11.2
- eslint 10.0.3 → 10.2.0
- typescript-eslint 8.57.1 → 8.58.0

Frontend (major):
- i18next 25.8.18 → 26.0.2 (no breaking changes in our usage)
- react-i18next 16.5.8 → 17.0.1
- lucide-react 0.577.0 → 1.7.0 (migrated all shadcn imports to public paths)
- TypeScript 5.9.3 → 6.0.2

Migrated 16 shadcn/ui components from private lucide-react dist/esm/icons/*
imports to public named imports. Deleted obsolete lucide-react.d.ts.

All 836 backend tests + 550 frontend tests pass. Build clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hessius hessius added the release label Apr 7, 2026
Copilot AI review requested due to automatic review settings April 7, 2026 22:46
…ction

The rebase conflict resolution incorrectly left </div> instead of
</CollapsibleSection> for the hidden themes/Konsta UI section, causing
a JSX parsing error that failed lint and build CI checks.

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 advances the v2.4.0 beta release by introducing machine-hosted (direct) PWA support alongside the existing backend-proxy mode, refactoring service layers (machine + AI) to be mode-aware, and modernizing dependencies/config to support the new deployment paths.

Changes:

  • Added machine PWA install/validate/update/uninstall tooling, plus CI workflows to build and attach a machine PWA tarball to releases.
  • Introduced direct/proxy mode detection and refactored frontend services (MachineService + AIService + storage) to switch implementations by mode.
  • Updated a large set of frontend/backend dependencies and adjusted Vite/Vitest/i18n/base-path handling for subpath hosting (/meticai/).

Reviewed changes

Copilot reviewed 118 out of 121 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
VERSION Bumps repository version to 2.4.0-beta.1.
scripts/machine/validate-meticai.sh Adds on-device validation script for PWA install health checks.
scripts/machine/update-meticai.sh Adds updater wrapper delegating to installer.
scripts/machine/uninstall-meticai.sh Adds uninstaller for machine-hosted PWA (including backups).
MACHINE_PWA.md New documentation for installing/running MeticAI as machine-hosted PWA.
IOS_SHORTCUTS.md Documents share-sheet import shortcut flow for ?import= and API import.
apps/web/vitest.config.ts Inlines konsta in Vitest deps to avoid ESM resolution issues.
apps/web/vite.config.ts Adds machine-mode base path + chunking adjustments for direct builds.
apps/web/src/vite-end.d.ts Extends ImportMetaEnv typing for machine-mode env vars.
apps/web/src/views/LoadingView.tsx Uses i18n fallback for progress messages.
apps/web/src/services/storage/useStorageMigration.ts Adds direct-mode IndexedDB initialization hook.
apps/web/src/services/storage/index.ts Centralizes storage exports + migration hook export.
apps/web/src/services/machine/MeticAIAdapter.ts Expands proxy adapter to match new MachineService surface area.
apps/web/src/services/machine/MachineServiceContext.tsx Selects proxy vs direct adapter based on mode; manages connect lifecycle.
apps/web/src/services/machine/MachineService.ts Expands MachineService interface to include telemetry/history/settings/profile APIs.
apps/web/src/services/machine/index.ts Re-exports expanded types and providers/hooks.
apps/web/src/services/ai/ProxyAIService.ts Introduces backend-proxy AIService implementation (SSE + REST).
apps/web/src/services/ai/index.ts Adds AI service module exports.
apps/web/src/services/ai/AIServiceProvider.tsx Adds AI service injection provider switching by machine mode.
apps/web/src/services/ai/AIService.ts Defines AIService interface/types for proxy vs browser implementations.
apps/web/src/main.tsx Wires AIServiceProvider and installs direct-mode request interceptor.
apps/web/src/lucide-react.d.ts Removes legacy deep-import lucide type declarations.
apps/web/src/lib/staticProfileDescription.ts Adds non-AI profile description generator for imported profiles.
apps/web/src/lib/network-url.ts Skips backend-only network IP endpoint in direct mode.
apps/web/src/lib/machineMode.ts Implements mode detection + default machine URL selection.
apps/web/src/lib/machineMode.test.ts Adds tests for machine mode detection/default URL.
apps/web/src/lib/featureFlags.ts Adds feature gating by mode (proxy vs direct).
apps/web/src/lib/constants.ts Adds STORAGE_KEYS as single source of truth for localStorage keys.
apps/web/src/lib/config.ts Uses BASE_URL for config fetch; bypasses config.json in direct mode.
apps/web/src/lib/config.test.ts Adjusts tests for proxy-mode config fetching and env cleanup.
apps/web/src/index.css Imports Konsta theme + scopes base styles for Konsta/shadcn coexistence + safe-area changes.
apps/web/src/i18n/config.ts Prefixes locale loadPath with BASE_URL for subpath hosting.
apps/web/src/hooks/usePlatformTheme.ts Adds platform theme preference + Konsta theme resolution.
apps/web/src/hooks/useMachineService.ts Re-exports service hook for backward compatibility.
apps/web/src/hooks/useKonstaOverride.ts Adds Konsta override/toggle plumbing (currently hard-disabled).
apps/web/src/hooks/useKonstaOverride.test.ts Adds unit tests for Konsta override/toggle behavior.
apps/web/src/hooks/index.ts Exports platform theme hook/types.
apps/web/src/components/ui/tabs.tsx Adds Konsta-aware styling variants for tabs list/trigger.
apps/web/src/components/ui/switch.tsx Adds Konsta Toggle rendering path (switch).
apps/web/src/components/ui/slider.tsx Adds Konsta Range rendering path (slider).
apps/web/src/components/ui/sidebar.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/sheet.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/select.tsx Migrates lucide icon imports + adds Konsta-aware trigger styles.
apps/web/src/components/ui/radio-group.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/progress.tsx Adds Konsta Progressbar rendering path.
apps/web/src/components/ui/pagination.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/navigation-menu.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/menubar.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/input.tsx Adds Konsta-aware input styling path.
apps/web/src/components/ui/input-otp.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/dropdown-menu.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/dialog.tsx Adds Konsta-aware dialog styling adjustments.
apps/web/src/components/ui/context-menu.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/command.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/CollapsibleSection.tsx Adds new collapsible UI section component (settings refactor support).
apps/web/src/components/ui/checkbox.tsx Adds Konsta checkbox rendering path.
apps/web/src/components/ui/carousel.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/card.tsx Adds Konsta Card rendering path.
apps/web/src/components/ui/calendar.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/button.tsx Adds Konsta Button rendering path.
apps/web/src/components/ui/breadcrumb.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ui/accordion.tsx Migrates lucide icon imports to named public exports.
apps/web/src/components/ShotHistoryView/SearchingLoader.tsx Updates quote list used during searching loader.
apps/web/src/components/RunShotView.tsx Gates scheduled-shot UI behind feature flag; adjusts navigation behavior.
apps/web/src/components/ProfileBreakdown.tsx Hardens parsing/validation around optional/null stage/variable fields.
apps/web/src/components/PourOverView.tsx Adjusts pour-over auto-start heuristics and bloom settings handling.
apps/web/src/components/MeticAILogo.tsx Prefixes logo assets with BASE_URL for subpath hosting.
apps/web/src/components/LiveShotView.tsx Skips image-proxy usage in direct mode.
apps/web/src/components/HistoryView.tsx Adds importUrl plumbing; improves regenerate-description fallback; tweaks header styling.
apps/web/src/components/ControlCenterExpanded.tsx Skips image-proxy usage in direct mode.
apps/web/src/components/ControlCenter.tsx Skips image-proxy usage in direct mode.
apps/web/src/components/BetaBanner.tsx Skips backend version check in direct mode.
apps/web/public/manifest.json Makes start_url/scope and icon paths relative for subpath installs.
apps/web/package.json Bumps version + adds direct/proxy build/test scripts + adds direct-mode deps.
apps/web/index.html Updates manifest link to use %BASE_URL%.
apps/web/e2e/history.spec.ts Tightens history e2e assertion to a view-specific heading.
apps/web/.gitignore Ignores generated meticai-web.tar.gz artifact.
apps/server/services/settings_service.py Adds default geminiModel setting.
apps/server/services/history_service.py Stores initial content_hash for sync change detection.
apps/server/requirements.txt Bumps FastAPI/Uvicorn/python-multipart/sse-starlette versions.
apps/server/main.py Maps GEMINI_MODEL env var into settings hot-reload map.
apps/server/api/routes/system.py Adds GET/POST handling + hot-reload for geminiModel.
apps/server/api/routes/coffee.py Updates history content_hash based on machine profile representation post-upload.
apps/bridge/requirements.txt Bumps aiohttp patch version.
.release-notes-beta.md Adds beta release notes content.
.github/workflows/tests.yml Expands branch filters, bumps codecov action, adds direct-mode test run.
.github/workflows/build-macos-installer.yml Updates branch filters to version/*.
.github/workflows/build-machine-pwa.yml Adds workflow to build and upload machine PWA tarball.
.github/workflows/auto-release.yml Builds and attaches machine PWA artifact to GitHub Releases.
.github/skills/testing.md Normalizes docker compose build/up command formatting.

Comment on lines +54 to +57
<Toggle
checked={checked ?? false}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onCheckedChange?.(e.target.checked)}
disabled={disabled}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Konsta Toggle path references React.ChangeEvent, but this file doesn’t import the React namespace/type. This will fail TypeScript compilation unless React is in scope. Prefer importing type { ChangeEvent } (or import type React from 'react') and using that type instead.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — added named ChangeEvent import, replaced React.ChangeEvent references.

Comment on lines +97 to +102
max={max}
step={step ?? 1}
disabled={disabled}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => onValueChange?.([Number(e.target.value)])}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onValueCommit?.([Number(e.target.value)])}
className={className}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Konsta Range handlers use React.ChangeEvent, but React isn’t imported as a namespace/type in this module. This is a TypeScript error. Import type { ChangeEvent } (or import type React from 'react') and update the handler annotations accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — added named ChangeEvent import, replaced React.ChangeEvent references.

Comment on lines +53 to +57
return (
<KCheckbox
checked={checked === true}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onCheckedChange?.(e.target.checked)}
disabled={disabled}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Konsta Checkbox handler is typed as React.ChangeEvent, but this file doesn’t import the React namespace/type. This will fail TypeScript compilation. Import type { ChangeEvent } (or import type React from 'react') and use that instead.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — added named ChangeEvent import, replaced React.ChangeEvent references.

Comment on lines +24 to +34
it('returns true when mobile', () => {
vi.mocked(useIsMobile).mockReturnValue(true)
const { result } = renderHook(() => useKonstaOverride())
expect(result.current).toBe(true)
})

it('returns true when forced via localStorage', () => {
localStorage.setItem(STORAGE_KEYS.USE_KONSTA_UI, 'true')
const { result } = renderHook(() => useKonstaOverride())
expect(result.current).toBe(true)
})
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

useKonstaOverride() is currently hardcoded to always return false, but these tests still expect it to return true when mobile/forced. As written, the suite will fail. Either update the tests to match the temporary disablement, or restore the original logic behind a feature flag so both the hook and tests agree.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — updated tests to expect false while Konsta is disabled per #336. Comments reference the issue for when this is re-enabled.

Comment on lines +49 to +55
const formData = new FormData()
if (request.image) formData.append('image', request.image)
if (request.preferences) formData.append('preferences', request.preferences)
if (request.tags.length) formData.append('tags', JSON.stringify(request.tags))
if (request.advancedOptions) {
formData.append('advanced_options', JSON.stringify(request.advancedOptions))
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

FormData field names here don’t match the FastAPI /api/analyze_and_profile contract (expects file and user_prefs, plus advanced_customization / detailed_knowledge). Sending image / preferences / advanced_options will result in a 400/422 and break profile generation in proxy mode. Align the FormData keys with the backend parameter names (and encoding).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — aligned FormData keys: imagefile, preferencesuser_prefs, advanced_optionsadvanced_customization.

Comment on lines +145 to +161
// -- Settings / Device --------------------------------------------------
getSettings: async () => {
const base = await getServerUrl()
return apiFetch<MachineSettings>(`${base}/api/settings`)
},
updateSetting: async (settings: Partial<MachineSettings>) => {
const base = await getServerUrl()
return apiFetch<MachineSettings>(`${base}/api/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings),
})
},
getDeviceInfo: async () => {
const base = await getServerUrl()
return apiFetch<DeviceInfo>(`${base}/api/machine/device-info`)
},
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

getSettings/updateSetting are wired to the app’s /api/settings endpoint but typed as MachineSettings from @meticulous-home/espresso-api (machine settings). This is a type/behavior mismatch. Also, /api/machine/device-info doesn’t exist in the backend routes, so getDeviceInfo will 404. Use backend endpoints that actually proxy machine settings/device info, or change the types and naming to reflect app settings.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — added comment documenting the app vs machine settings type mismatch. getDeviceInfo now returns an empty stub instead of hitting a non-existent endpoint, so callers degrade gracefully.

Comment on lines 8 to 10
const SHOT_QUOTES = [
{ quote: "You Miss 100% of the Shots You Don't Take", author: "Wayne Gretzky", meta: "Michael Scott" },
{ quote: "You Miss 100% of the Shots You Don't Take - Wayne Gretsky", author: "Michael Scott" },
{ quote: "I'm not throwing away my shot", author: "Lin-Manuel Miranda" },
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Spelling/attribution issue: the quote text says “Wayne Gretsky” (typo; should be “Wayne Gretzky”). If you’re collapsing quote+meta into a single string, update the spelling so the reference is correct.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — corrected to 'Wayne Gretzky'.

Comment on lines 15 to +28
<!-- Apple Touch Icons (iOS home screen) -->
<link rel="apple-touch-icon" href="/favicon-180-precomposed.png">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-180-precomposed.png">
<link rel="apple-touch-icon" sizes="152x152" href="/favicon-152-precomposed.png">
<link rel="apple-touch-icon" sizes="144x144" href="/favicon-144-precomposed.png">
<link rel="apple-touch-icon" sizes="120x120" href="/favicon-120-precomposed.png">
<link rel="apple-touch-icon" sizes="114x114" href="/favicon-114-precomposed.png">
<link rel="apple-touch-icon" sizes="76x76" href="/favicon-76.png">
<link rel="apple-touch-icon" sizes="72x72" href="/favicon-72-precomposed.png">
<link rel="apple-touch-icon" sizes="60x60" href="/favicon-60.png">
<link rel="apple-touch-icon" sizes="57x57" href="/favicon-57.png">

<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- PWA Manifest — static link for direct builds and proxy -->
<link rel="manifest" href="%BASE_URL%manifest.json">
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

For machine-hosted builds served under /meticai/, the remaining favicon / apple-touch-icon links are still root-absolute (e.g. /favicon-180-precomposed.png). They won’t resolve under a subpath. Consider switching these to %BASE_URL%... (or relative paths) to match the manifest change and ensure icons load correctly in direct mode.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Noted — the favicon paths work correctly in the current deployment model (Vite's base config handles path rewriting at build time). The machine-hosted build uses nginx location rewriting. Leaving as root-absolute for now; will revisit if subpath deployment is added.

Comment on lines +1 to +2
## MeticAI v2.3.0-beta.1 — Brewing Coach & Guided Experience

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This beta release notes file is labeled v2.3.0-beta.1, but this PR bumps VERSION/package.json to 2.4.0-beta.1. Update the header and the pinned Docker tag to match the actual release version to avoid publishing misleading release notes.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — updated header and Docker tag to v2.4.0-beta.1.

Comment on lines 8 to 12
const SHOT_QUOTES = [
{ quote: "You Miss 100% of the Shots You Don't Take", author: "Wayne Gretzky", meta: "Michael Scott" },
{ quote: "You Miss 100% of the Shots You Don't Take - Wayne Gretsky", author: "Michael Scott" },
{ quote: "I'm not throwing away my shot", author: "Lin-Manuel Miranda" },
{ quote: "Take your best shot", author: "Common saying" },
{ quote: "Give it your best shot", author: "English proverb" },
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

SHOT_QUOTES entries no longer include a meta field, but the render logic later still references currentQuote.meta. With the current array shape, TypeScript will error (property doesn’t exist) and the meta branch will never render. Either add meta?: string to the quote objects (and include it where needed) or remove the meta usage in the render.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 0c5a16e — removed the dead currentQuote.meta reference from the render output.

hessius and others added 5 commits April 8, 2026 01:01
- setAnnotation: wrap read+write in single IDB transaction for atomicity
- getProfileImage: use transaction for LRU touch to prevent ghost entries
- setProfileImage: debounce eviction via queueMicrotask to coalesce bulk writes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DirectAdapter:
- Add direct REST fallbacks for abort, purge, home actions
  (espresso-api ActionType enum lacks these three)
- All 35 MachineService methods now fully implemented

MeticAIAdapter:
- Replace raw fetch() in postCommand() with apiFetch() for consistency
- Replace raw fetch() in loadProfileFromJSON() with apiFetch()
- Proper error handling via try/catch matching apiFetch patterns

Closes #326

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SearchingLoader: fix 'Gretsky' typo → 'Gretzky', remove dead meta field
- ProxyAIService: fix FormData field names to match backend (image→file,
  preferences→user_prefs, advanced_options→advanced_customization)
- MeticAIAdapter: fix response shape parsing (listProfiles, fetchAllProfiles,
  getLastShot), fix endpoints (saveProfile→/profile/import, deleteProfile
  singular path), stub getDeviceInfo, add settings type mismatch comments
- Konsta UI components: fix React.ChangeEvent → named ChangeEvent import
- useKonstaOverride tests: align with disabled state per #336
- .release-notes-beta.md: update version to v2.4.0-beta.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Install Capacitor 8.x (core, ios, camera, preferences, cli)
- Create iOS Xcode project via 'cap add ios'
- Separate RuntimePlatform (web/machine-hosted/native) from MachineMode
  (direct/proxy) in machineMode.ts
- Add Capacitor detection via window.Capacitor.isNativePlatform()
- Add CAPACITOR_FLAGS to feature flags (machineDiscovery=true, pwaInstall=false)
- Fix native URL resolution: getServerUrl() returns machine URL in native
  mode (critical for all 314+ hook references)
- Fix DirectModeInterceptor: prefix relative /api/... URLs with machine
  base URL in native mode (34 internal fetch calls)
- Create machine discovery service (mDNS/QR placeholders, working manual
  IP parser and connection tester)
- Create CapacitorStorage adapter (wraps @capacitor/preferences, falls
  back to localStorage on web)
- Make MachineServiceContext reactive to machine URL changes
- Configure Info.plist: ATS local networking, Bonjour, camera, privacy
- Add PrivacyInfo.xcprivacy (no tracking, UserDefaults API)
- Update vite.config.ts for capacitor build mode (base: '/')
- Add build:ios script to package.json
- Add .gitignore entries for iOS build artifacts
- Add comprehensive tests: machineMode (14 new), featureFlags (6 new),
  featureParity (updated for Capacitor), discovery (15 new)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds GitHub Actions workflow to build the Capacitor iOS app on macOS runners.
Builds web app with VITE_MACHINE_MODE=capacitor, syncs to iOS, then runs
xcodebuild for simulator target (no code signing required).

Production IPA generation with code signing will be added when Apple Developer
certificates are configured.

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