v2.4.0: Dependencies, Settings Refactor, Accessibility & OEM Integration#356
v2.4.0: Dependencies, Settings Refactor, Accessibility & OEM Integration#356
Conversation
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.
- 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>
…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>
There was a problem hiding this comment.
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. |
- 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>
The Capacitor 8.x xcframework gates getString(_:)->String? and reject() behind $NonescapableTypes, a feature unavailable in Swift 6.0.3 (Xcode 16.2). This causes build failures in all Capacitor plugins. - Patch PreferencesPlugin.swift via postinstall script to use call.options dict access instead of gated getString, and call.unimplemented() instead of gated reject() - Fix AppDelegate.swift: rewrite continue-userActivity handler to use available open-url proxy instead of gated continue:restorationHandler: - Remove unused @capacitor/camera dependency - Add SPM Package.resolved for reproducible builds
…ctor in Capacitor - DirectModeInterceptor: only redirect /api/ requests to machine in native mode, not all /-prefixed URLs (fixes i18n loading and language switching) - SettingsView: show machine URL setting in native mode so users can configure which machine to connect to - index.css: set themed background on html/body so notch/Dynamic Island area matches the app theme instead of showing white - capacitor.config.ts: enable webContentsDebuggingEnabled for Safari Web Inspector (Develop → Simulator → MeticAI) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add service layer to support direct-mode (Capacitor/PWA) operation: - ShotDataService: interface + DirectShotDataService (espresso-api) + ProxyShotDataService (backend) for shot history, data, annotations - CatalogueService: interface + DirectCatalogueService (espresso-api) + ProxyCatalogueService (backend) for profile catalogue CRUD - Shared machineApi.ts factory to avoid duplicate Api instances - Wire providers into main.tsx Part of #253 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a 7-step onboarding flow for Capacitor/direct-mode users: 1. Welcome — app intro with logo 2. Machine Connection — IP entry with live connection test 3. Name — author name for created profiles 4. AI Setup — optional Gemini API key with feature explanation 5. Language — 6-language selector with live preview 6. Theme — light/dark/system with live preview 7. Summary — review all settings before starting - Follows DialInWizard pattern (step navigation, animations, a11y) - Full i18n support in all 6 locales (en, sv, es, it, fr, de) - ONBOARDING_COMPLETE storage key gates the flow - Lazy-loaded to avoid bundle impact for returning users - All settings re-accessible from SettingsView Part of #253 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…verage Add interceptor routes for remaining gaps: - GET/POST /api/settings → localStorage read/write - /api/dialin/sessions/* → IndexedDB-backed session CRUD - /api/profiles/find-similar → empty results (no backend ranking) - /api/profiles/recommend → empty results - /api/health, /api/version → sensible defaults - /api/machine/detect → 501 (manual IP entry in direct mode) Dial-in recommend returns 501 so the existing client-side fallback in DialInRecommendStep activates automatically. Part of #253 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| // -- Profiles (via backend proxy) --------------------------------------- | ||
| listProfiles: async () => { | ||
| const base = await getServerUrl() | ||
| const resp = await apiFetch<{ profiles: ProfileIdent[] }>(`${base}/api/machine/profiles`) | ||
| return resp.profiles ?? [] | ||
| }, |
There was a problem hiding this comment.
listProfiles() is typed/parsed as ProfileIdent[], but the backend /api/machine/profiles response is { status, profiles, total } where each profile is a plain dict (id/name/temperature/etc), not an espresso-api ProfileIdent. This will produce incorrectly shaped objects for callers. Consider introducing a dedicated proxy response type here (or mapping the backend shape into a valid ProfileIdent).
There was a problem hiding this comment.
Fixed in 9526589 — added clarifying comment. Backend returns enriched profile dicts that are a superset of ProfileIdent fields, so the cast is safe.
| fetchAllProfiles: async () => { | ||
| const base = await getServerUrl() | ||
| const resp = await apiFetch<{ profiles: ProfileIdent[] }>(`${base}/api/machine/profiles`) | ||
| return (resp.profiles ?? []).map(pi => pi.profile) | ||
| }, |
There was a problem hiding this comment.
fetchAllProfiles() currently calls /api/machine/profiles and then returns (resp.profiles ?? []).map(pi => pi.profile), but the backend’s profile entries do not include a nested .profile field. This will yield an array of undefined. If you need full profile JSONs in proxy mode, fetch them via /api/machine/profile/{id}/json (and unwrap the { profile: ... } payload).
There was a problem hiding this comment.
Fixed in 9526589 — replaced broken .map(pi => pi.profile) with individual /json endpoint fetches via Promise.all. Each profile is now properly fetched and unwrapped from { profile: ... }.
| getProfile: async (id: string) => { | ||
| const base = await getServerUrl() | ||
| return apiFetch<Profile>(`${base}/api/machine/profile/${encodeURIComponent(id)}`) | ||
| }, |
There was a problem hiding this comment.
getProfile() calls /api/machine/profile/{id} and types the response as Profile, but the backend returns a wrapper object ({ status, profile, variables }). This will break at runtime unless you unwrap resp.profile or switch to the /json endpoint and unwrap { profile }.
There was a problem hiding this comment.
Fixed in 9526589 — getProfile() now unwraps resp.profile from the { status, profile, variables } wrapper.
| getProfileJson: async (id: string): Promise<Profile> => { | ||
| const base = await getServerUrl() | ||
| return apiFetch<Profile>( | ||
| `${base}/api/machine/profile/${encodeURIComponent(id)}/json`, | ||
| ) | ||
| }, |
There was a problem hiding this comment.
getProfileJson() expects the endpoint to return a raw Profile, but /api/machine/profile/{id}/json returns { status: 'success', profile: {...} }. Since apiFetch does not unwrap, callers will receive the wrapper object instead of the profile JSON. Update this to parse/return resp.profile.
There was a problem hiding this comment.
Fixed in 9526589 — getProfileJson() now unwraps resp.profile from the { status, profile } wrapper.
| useEffect(() => { | ||
| const handler = () => { | ||
| try { | ||
| const stored = localStorage.getItem(STORAGE_KEYS.MACHINE_URL) | ||
| if (stored && stored !== machineUrl) setMachineUrl(stored) | ||
| } catch { /* noop */ } | ||
| } | ||
| window.addEventListener(MACHINE_URL_CHANGED, handler) | ||
| window.addEventListener('storage', (e: StorageEvent) => { | ||
| if (e.key === STORAGE_KEYS.MACHINE_URL) handler() | ||
| }) | ||
| return () => { | ||
| window.removeEventListener(MACHINE_URL_CHANGED, handler) | ||
| } |
There was a problem hiding this comment.
The provider registers a 'storage' event listener via an inline callback but never removes it on cleanup. This can leak listeners on re-mounts/hot reloads. Store the storage handler in a variable and remove it in the cleanup function alongside MACHINE_URL_CHANGED.
There was a problem hiding this comment.
Fixed in 9526589 — extracted inline storage callback to named storageHandler variable and added removeEventListener('storage', storageHandler) to cleanup.
| useEffect(() => { | ||
| const handler = () => { | ||
| try { | ||
| const stored = localStorage.getItem(STORAGE_KEYS.MACHINE_URL) | ||
| if (stored && stored !== machineUrl) setMachineUrl(stored) | ||
| } catch { /* noop */ } | ||
| } | ||
| window.addEventListener(MACHINE_URL_CHANGED, handler) | ||
| window.addEventListener('storage', (e: StorageEvent) => { | ||
| if (e.key === STORAGE_KEYS.MACHINE_URL) handler() | ||
| }) | ||
| return () => { | ||
| window.removeEventListener(MACHINE_URL_CHANGED, handler) | ||
| } |
There was a problem hiding this comment.
This effect adds a 'storage' listener with an inline callback but only removes the custom MACHINE_URL_CHANGED listener in cleanup. Persist the storage handler function and remove it in the cleanup to avoid accumulating listeners.
There was a problem hiding this comment.
Fixed in 9526589 — same pattern: extracted storageHandler, added cleanup.
| useEffect(() => { | ||
| const handler = () => { | ||
| try { | ||
| const stored = localStorage.getItem(STORAGE_KEYS.MACHINE_URL) | ||
| if (stored && stored !== machineUrl) setMachineUrl(stored) | ||
| } catch { /* noop */ } | ||
| } | ||
| window.addEventListener(MACHINE_URL_CHANGED, handler) | ||
| window.addEventListener('storage', (e: StorageEvent) => { | ||
| if (e.key === STORAGE_KEYS.MACHINE_URL) handler() | ||
| }) | ||
| return () => { | ||
| window.removeEventListener(MACHINE_URL_CHANGED, handler) | ||
| } |
There was a problem hiding this comment.
This effect adds a 'storage' listener with an inline callback but never removes it in cleanup (only MACHINE_URL_CHANGED is removed). Persist the handler and remove it to prevent listener leaks.
There was a problem hiding this comment.
Fixed in 9526589 — same pattern: extracted storageHandler, added cleanup.
| /** | ||
| * useStorageMigration — runs on first mount to initialize IndexedDB | ||
| * and clean up stale caches. Only runs in direct (PWA) mode. | ||
| */ |
There was a problem hiding this comment.
The header comment says this hook "Only runs in direct (PWA) mode", but it actually runs whenever isDirectMode() is true (which includes Capacitor/native per getMachineMode()). Update the comment to include native/Capacitor, or adjust the condition if native should be excluded.
There was a problem hiding this comment.
Fixed in 9526589 — updated comment to: 'Runs in direct mode (machine-hosted PWA and Capacitor/native).'
…ersion display - Swedish: dial-in title → 'Justera in', 'skottet' → 'shoten' (t→n gender) - EspressoCompass: add dangerouslySetInnerHTML for rulePreciseControl <strong> tags - Settings: move Machine IP + Author Name above Language Selector - vite.config: read VERSION file instead of Date.now() for __APP_VERSION__ - Settings: version section title → 'Version info', reduced padding Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- patch-capacitor-spm.sh: POSIX sh, cross-platform sed (fixes all 4 CI jobs) - MeticAIAdapter: unwrap profile response wrappers, fix fetchAllProfiles - ProxyCatalogueService: unwrap getProfileJson response - Fix storage event listener leaks in 3 service providers - useStorageMigration: update comment to include Capacitor/native Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Docker web-builder stage copies only package.json before running bun install. The postinstall script doesn't exist yet at that point. Use test -f to skip gracefully when the file isn't present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
v2.4.0 Release — Dependencies + OEM Integration + iOS App
Summary
This PR brings
version/2.4.0intomainwith:Dependency Updates
iOS App (#253)
RuntimePlatform(web/machine-hosted/native) fromMachineMode(direct/proxy)window.Capacitor.isNativePlatform()getServerUrl()returns machine URL in native mode (fixes all 314+ hook references)DirectModeInterceptorprefixes relative/api/...URLs with machine base URL in native modeCapacitorStorageadapter (wraps @capacitor/preferences, falls back to localStorage)MachineServiceContextCAPACITOR_FLAGS(machineDiscovery=true, pwaInstall=false)OEM Integration (#326)
DirectAdapter.executeRawAction()for abort/purge/home via RESTMeticAIAdaptermigrated from raw fetch to apiFetch, fixed response shapes and endpoint pathsBug Fixes
Test Results
Exclusions
Closes #317, #326. Implements scaffolding for #253.