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.
- 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
- 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>
…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. |
| <Toggle | ||
| checked={checked ?? false} | ||
| onChange={(e: React.ChangeEvent<HTMLInputElement>) => onCheckedChange?.(e.target.checked)} | ||
| disabled={disabled} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 0c5a16e — added named ChangeEvent import, replaced React.ChangeEvent references.
| 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} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 0c5a16e — added named ChangeEvent import, replaced React.ChangeEvent references.
| return ( | ||
| <KCheckbox | ||
| checked={checked === true} | ||
| onChange={(e: React.ChangeEvent<HTMLInputElement>) => onCheckedChange?.(e.target.checked)} | ||
| disabled={disabled} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 0c5a16e — added named ChangeEvent import, replaced React.ChangeEvent references.
| 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) | ||
| }) |
There was a problem hiding this comment.
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.
| 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)) | ||
| } |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Fixed in 0c5a16e — aligned FormData keys: image→file, preferences→user_prefs, advanced_options→advanced_customization.
| // -- 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`) | ||
| }, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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" }, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 0c5a16e — corrected to 'Wayne Gretzky'.
| <!-- 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"> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
.release-notes-beta.md
Outdated
| ## MeticAI v2.3.0-beta.1 — Brewing Coach & Guided Experience | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 0c5a16e — updated header and Docker tag to v2.4.0-beta.1.
| 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" }, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 0c5a16e — removed the dead currentQuote.meta reference from the render output.
- 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>
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.