feat: MeticAI v2.3.0 — Brewing Coach & Guided Experience#285
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…odules Consolidate duplicated code from ExpertAnalysisView.tsx and LlmAnalysisModal.tsx: - parseStructuredAnalysis(), SECTION_STYLES, getSectionStyle(), CIRCLED_NUMBERS → lib/parseAnalysis.ts - SectionCard component → components/SectionCard.tsx - Both consumers reduced by ~50% (313→151 and 316→149 lines) - Array-based SECTION_STYLES with pattern matching for extensibility - Pre-includes 6th 'Taste-Based' entry for upcoming Espresso Compass (#261) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add RECOMMENDATIONS_JSON block to LLM analysis prompt (shots.py)
- Add POST /shots/analyze-recommendations endpoint to extract and
classify structured recommendations from cached analysis
- Add POST /profile/{name}/apply-recommendations endpoint to apply
selected recommendations to profiles with safety guards
- Add parseRecommendationsJSON() and hasRecommendations() to frontend
- Create RecommendationSelectionDialog component with checkbox
selection, confidence badges, stage grouping, and apply flow
- Integrate Apply Recommendations button into ExpertAnalysisView
- Add i18n keys for all 6 locales (en, de, es, fr, it, sv)
- Add backend tests: parsing, classification, both endpoints (15 tests)
- Add frontend tests: parseRecommendationsJSON, hasRecommendations (9 tests)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…LM re-ranking - Add ProfileRecommendationService with two-tier scoring (local Jaccard similarity + optional Gemini LLM re-ranking) and LRU cache - Add ProfileRecommendations component in FormView (debounced, ≥2 tags) - Add FindSimilarOverlay dialog in ProfileCatalogueView - Add 29 backend tests for scoring functions, cache, and service integration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#175) Break the 2,634-line ShotHistoryView.tsx monolith into a well-structured ShotHistoryView/ directory with focused sub-components: - types.ts: All shared type definitions and SPEED_OPTIONS constant - shotDataTransforms.ts: Pure data transform functions (getChartData, getStageRanges, mergeWithTargetCurves, getComparisonChartData, etc.) - useReplayAnimation.ts: Custom hook deduplicating replay animation logic - SearchingLoader.tsx: Loading animation with progress bar and quotes - ShotList.tsx: Shot list view with annotation indicators - ShotDetail.tsx: Full detail view with tabs, replay, compare, analyze - ShotHistoryView.tsx: Lean orchestrator (~170 lines, well under 1,000) - index.ts: Barrel export preserving existing import paths Zero recharts imports outside charts/. No consumer changes needed. Build, lint (0 errors), and all 303 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- TasteCompassInput: interactive 2D SVG drag compass + taste descriptors - build_taste_context() in prompt_builder.py with Espresso Compass domain knowledge - compute_taste_hash() in gemini_service.py for cache differentiation - Backend tests (32 passing) and frontend tests (17 passing) - i18n keys for all 6 locales (en, de, es, fr, it, sv) - Backward compatible: all new params optional with None defaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add full-stack support for adjusting profile variables before running espresso shots, with three save modes after shot completion. Backend: - Extend temp_profile_service with MeticAI Override prefix and apply_variable_overrides() for deep-copying profiles with modified variable values (skips info_ display-only variables) - Add run-profile-with-overrides endpoint to scheduling.py with dual route registration, supporting save_mode: none/save_original/ save_new via FormData Frontend: - Create VariableAdjustPanel component with collapsible UI, shadcn Slider controls grouped by variable type, diff indicators, and individual/bulk reset - Integrate panel into RunShotView with overrides state management, modified run flow (POST to overrides endpoint when adjustments exist), and post-shot save mode dialog with 30s auto-dismiss Tests: - 9 backend tests: 4 unit tests for apply_variable_overrides + 5 endpoint tests covering all save modes and error cases - 9 frontend tests: rendering, expansion, reset behavior, info_ filtering, badge display, null returns for empty variables Closes #281 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements a 7-step guided espresso dial-in workflow: - Coffee details → Profile selection → Preparation checklist → Pull shot → Taste feedback (compass) → Recommendations → History Backend: - Pydantic models (RoastLevel, CoffeeProcess, DialInSession, etc.) - Session service with in-memory store + JSON file persistence - 7 dual-registered API endpoints (/dialin/* and /api/dialin/*) - Prompt builder function for AI recommendations - 17 new backend tests (791 total passing) Frontend: - DialInWizard container with step state machine - 7 step components with full i18n support - TasteCompassInput integration for taste feedback - Heuristic-based recommendation engine - ViewState routing + StartView button integration i18n: All 6 locales (en, de, es, fr, it, sv) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
) - Add <header>, <main>, <nav> landmarks and <SkipNavigation> to App.tsx - Integrate useReducedMotion for motion-safe transitions - Add aria-label to all icon-only buttons (theme toggle, QR, back buttons) - Make title click accessible with role=button, tabIndex, onKeyDown - i18n: add a11y.* keys to all 6 locales (en, de, es, fr, it, sv) - SkipNavigation: use i18n for link labels - EspressoChart: add role=img with aria-label - DialInWizard: add screen reader step announcements, progressbar attrs - VariableAdjustPanel: add aria-expanded, aria-controls, slider/reset labels - RecommendationSelectionDialog: add role=group + aria-labelledby to stages - ShotList: add role=list/listitem semantics, keyboard support - SearchingLoader: add aria-live, aria-busy, progress attributes - ExpertAnalysisView: add aria-live to loading state, back button label - ProfileRecommendations: add aria-busy to loading skeleton - FindSimilarOverlay: add role=button, keyboard support, aria-label to cards - Update test to match aria-label change on reset button Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR bumps the project to 2.3.0-beta.1 and introduces a set of new UX and AI-assisted features across the web UI and FastAPI backend, centered on dial-in workflows, profile recommendations, structured analysis parsing, and accessibility improvements.
Changes:
- Add a Dial-In Wizard flow (UI + backend session API/models) and new navigation entry.
- Add profile recommendation and find similar profile UI backed by new/extended backend services/endpoints.
- Refactor expert/LLM analysis rendering and add parsing utilities/tests for structured sections + recommendations JSON, plus additional a11y/i18n updates.
Reviewed changes
Copilot reviewed 58 out of 59 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSION | Bump root version to 2.3.0-beta.1. |
| apps/web/package.json | Bump web app version to 2.3.0-beta.1. |
| apps/web/src/views/StartView.tsx | Add “Dial-In” entry point on the start screen. |
| apps/web/src/views/FormView.tsx | Insert profile recommendations into generation form flow. |
| apps/web/src/types/index.ts | Add dial-in to ViewState. |
| apps/web/src/lib/parseAnalysis.ts | New parsing utilities for structured analysis + recommendations JSON. |
| apps/web/src/lib/parseAnalysis.test.ts | Unit tests for recommendation JSON parsing helpers. |
| apps/web/src/components/VariableAdjustPanel.tsx | New variable override adjustment UI. |
| apps/web/src/components/VariableAdjustPanel.test.tsx | Tests for VariableAdjustPanel behavior. |
| apps/web/src/components/TasteCompassInput.test.tsx | Tests for taste compass input component. |
| apps/web/src/components/SkipNavigation.tsx | Localize skip links via i18n. |
| apps/web/src/components/ShotHistoryView/useReplayAnimation.ts | New replay animation hook for shot history UI. |
| apps/web/src/components/ShotHistoryView/types.ts | Add types/constants used in shot history view. |
| apps/web/src/components/ShotHistoryView/ShotList.tsx | New shot list UI with a11y improvements. |
| apps/web/src/components/ShotHistoryView/ShotHistoryView.tsx | New shot history view container with stale-while-revalidate pattern. |
| apps/web/src/components/ShotHistoryView/SearchingLoader.tsx | New loader component (progress + quotes) for shot history. |
| apps/web/src/components/ShotHistoryView/index.ts | Export ShotHistoryView barrel. |
| apps/web/src/components/SectionCard.tsx | Extract reusable analysis section card component. |
| apps/web/src/components/RecommendationSelectionDialog.tsx | New dialog for selecting/applying AI recommendations. |
| apps/web/src/components/ProfileRecommendations.tsx | New component calling backend to suggest matching profiles by tags. |
| apps/web/src/components/ProfileCatalogueView.tsx | Add “find similar” action + overlay integration. |
| apps/web/src/components/LlmAnalysisModal.tsx | Refactor to use parseAnalysis + SectionCard. |
| apps/web/src/components/FindSimilarOverlay.tsx | New overlay to show similar profiles via backend endpoint. |
| apps/web/src/components/ExpertAnalysisView.tsx | Refactor analysis rendering + add “apply recommendations” UI. |
| apps/web/src/components/DialInWizard.tsx | New dial-in wizard orchestration component. |
| apps/web/src/components/DialInCoffeeStep.tsx | Dial-in step: coffee details collection. |
| apps/web/src/components/DialInProfileStep.tsx | Dial-in step: pick profile or enter manually. |
| apps/web/src/components/DialInPrepStep.tsx | Dial-in step: preparation checklist. |
| apps/web/src/components/DialInBrewStep.tsx | Dial-in step: brew instruction screen. |
| apps/web/src/components/DialInTasteStep.tsx | Dial-in step: taste compass feedback submission. |
| apps/web/src/components/DialInRecommendStep.tsx | Dial-in step: recommendation display + server update. |
| apps/web/src/components/DialInHistoryStep.tsx | Dial-in step: iteration history review. |
| apps/web/src/components/charts/EspressoChart.tsx | Add aria-labels for chart accessibility. |
| apps/web/src/App.tsx | Add SkipNavigation, reduced-motion handling, and dial-in view routing. |
| apps/web/public/locales/en/translation.json | Add new strings for recommendations/dial-in/a11y/variables/taste. |
| apps/web/public/locales/sv/translation.json | Swedish translations for new keys. |
| apps/web/public/locales/de/translation.json | German translations for new keys. |
| apps/web/public/locales/es/translation.json | Spanish translations for new keys. |
| apps/web/public/locales/fr/translation.json | French translations for new keys. |
| apps/web/public/locales/it/translation.json | Italian translations for new keys. |
| apps/server/services/temp_profile_service.py | Add override prefix + apply_variable_overrides helper. |
| apps/server/services/gemini_service.py | Add taste hash helper for cache-key differentiation. |
| apps/server/prompt_builder.py | Add taste context builder + dial-in recommendation prompt builder. |
| apps/server/models/dialin.py | Add Pydantic models for dial-in sessions/iterations. |
| apps/server/services/dialin_service.py | New dial-in session persistence + state management service. |
| apps/server/api/routes/dialin.py | New dial-in CRUD API routes. |
| apps/server/api/routes/shots.py | Add taste-aware caching + structured recommendations extraction endpoint. |
| apps/server/api/routes/scheduling.py | Add “run profile with overrides” endpoint. |
| apps/server/api/routes/profiles.py | Invalidate recommendation cache on profile changes; add apply-recommendations + recommendation endpoints. |
| apps/server/main.py | Register new dial-in router. |
| apps/server/test_taste_compass.py | New backend tests for taste compass prompt + hashing. |
| apps/server/test_recommendations.py | New backend tests for profile recommendation service/helpers. |
You can also share your feedback on Copilot code review. Take the survey.
- Fix useScreenReaderAnnouncement destructuring bug (returned value, not object) - Pass missing i18n interpolation values (percent, step) to aria-labels - Add missing i18n keys: recommendations.globalSettings, a11y.recommendations.selectVariable, a11y.dialIn.submitProfileName, variables.type.* across all 6 locales - Replace hardcoded TYPE_LABELS with i18n-backed TYPE_LABEL_KEYS - Use getServerUrl() in ExpertAnalysisView instead of hardcoded /api/ URL - Use deep_convert_to_dict() in scheduling.py for profile serialization - Add aria-label to all icon-only buttons (ProfileCatalogue, DialInProfileStep) - Fix force_refresh logic in shots.py analyze-recommendations endpoint - Default is_patchable to true when missing from LLM output - Add error handling with toast in RecommendationSelectionDialog - Call dialin_service._load() during lifespan startup for session persistence - Add session bounds (MAX_SESSIONS=200) and TTL pruning (7 days) to dialin_service - Use get_logger() instead of logging.getLogger() in dialin_service - Add test_taste_compass.py and test_recommendations.py to CI pytest command Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
hessius
left a comment
There was a problem hiding this comment.
All 15 code review findings addressed in commit baa71da. Summary:
Runtime bugs fixed:
- useScreenReaderAnnouncement() destructuring (returned function, not object) — would have thrown at runtime
- Missing i18n interpolation values (percent, step) — screen readers would hear raw placeholders
- force_refresh logic in analyze-recommendations — was returning 404 instead of re-parsing
- vars() not deep-converting nested objects in scheduling.py — would cause AttributeError
- is_patchable defaulting to false when missing — made all recommendations appear non-actionable
- dialin_service._load() never called — sessions wouldn't restore after restart
Accessibility fixed:
- aria-labels added to all icon-only buttons (ProfileCatalogue 4 buttons, DialInProfileStep submit)
- All hardcoded English UI strings replaced with i18n t() calls + 9 new keys across 6 locales
Reliability fixed:
- Error handling added to RecommendationSelectionDialog (toast on failure)
- Session bounds (MAX_SESSIONS=200) and TTL pruning (7 days) in dialin_service
- dialin_service switched to get_logger() (service-layer convention)
- CI pytest command updated to include test_taste_compass.py and test_recommendations.py
Verification: 791 backend tests ✅ | 312 frontend tests ✅ | 0 lint errors ✅ | Build clean ✅
All 6 locales now have identical key coverage (1031 keys each). Added missing translations for: controlCenter.toasts, history.notes, and pourOver recipe mode keys across de, es, fr, it, sv. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comprehensive Code Review (Manual)After addressing all 15 automated review findings, I performed a thorough manual code review across all v2.3.0 changes. Here's what I found and fixed: Additional Issues Found & Fixedi18n completeness gap (d0750d8): All 6 locales now have identical key coverage (1031 keys each). Found 15 keys present in English but missing from de/es/fr/it/sv — these were pre-existing from v2.2.0 (controlCenter, history.notes, pourOver recipe mode keys). Added proper translations for all. Patterns Verified Clean
Risk AssessmentLow risk items:
Final State
|
- Add taste_x/taste_y bounds validation (-1 to 1) with HTTP 422 in shots.py
- Add descriptor validation against 16 allowed taste descriptors
- Change is_patchable default from true to false in parseAnalysis.ts
- Add AI-powered /api/dialin/sessions/{id}/recommend endpoint with rule-based fallback
- Wire DialInRecommendStep to call server endpoint instead of hardcoded rules
- Remove unused aiConfigured prop from DialInRecommendStep/DialInWizard
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create .github/skills/browser-testing.md with 12-section testing protocol - Add browser testing as quality gate #7 in CONVENTIONS.md - Reference browser-testing.md in release.md pre-release checklist - Add skill reference to copilot-instructions.md, AGENTS.md, CLAUDE.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Browser Testing Results — v2.3.0-beta.1Tested against Docker container running
Notes
Code Review Fixes Applied
CI NoteCI has not triggered on recent pushes — possibly GitHub Actions minutes quota. All tests pass locally: 852 backend + 312 frontend, lint clean, build succeeds. |
…analysis - Fix notes buttons overflow-x on narrow viewports (flex-wrap) (#175) - Move FindSimilar from catalogue to profile detail view (#95) - Add AI token disclaimer to FindSimilar overlay (#95) - Show profile images instead of score circles in FindSimilar (#95) - Make FindSimilar results always clickable with navigation (#95) - Fix FindSimilar modal overflow-x on narrow viewports (#95) - Only enable FindSimilar for profiles with descriptions (#95) - Include stage names in profile similarity scoring (#95) - Strip RECOMMENDATIONS_JSON from analysis section display (#258) - Fix is_patchable with fuzzy matching and default-true (#258) - Scroll to top on view transitions - Auto-navigate to live view on Run Now - Remove false emoji/unused variable warnings in ProfileBreakdown - Add profileRecommendations.aiDisclaimer to all 6 locales
Merges v2.2.2 bug fixes into the 2.3.0 development branch: - Gemini model update (gemini-2.0-flash → gemini-2.5-flash) - Model name resolution at call time with empty env handling Conflicts resolved: - VERSION: kept 2.3.0-beta.1 - apps/web/package.json: kept 2.3.0-beta.1 - ShotHistoryView.tsx: accepted deletion (decomposed in v2.3.0) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merge main into version/2.3.0: - Gemini model fix (gemini-2.0-flash → gemini-2.5-flash) - get_model_name() call-time resolution - Fix _MODEL_NAME → get_model_name() in profile_recommendation_service.py Python dependency bumps (3): - uvicorn 0.41.0 → 0.42.0 - sse-starlette 3.2.0 → 3.3.2 - zeroconf 0.134.0 → 0.148.0 NPM dependency bumps (28): Production (12): @tailwindcss/vite 4.2.1, @tanstack/react-query 5.90.21, framer-motion 12.37.0, i18next 25.8.18, i18next-browser-languagedetector 8.2.1, modern-screenshot 4.6.8, react 19.2.4, react-day-picker 9.14.0, react-dom 19.2.4, react-error-boundary 6.1.1, react-i18next 16.5.8, react-resizable-panels 4.7.3 Dev (16): @chromatic-com/storybook 5.0.1, @playwright/test 1.58.2, @storybook/addon-a11y 10.2.19, @storybook/addon-docs 10.2.19, @storybook/react-vite 10.2.19, @tailwindcss/postcss 4.2.1, @testing-library/react 16.3.2, @types/react 19.2.14, @vitejs/plugin-react-swc 4.3.0, @vitest/browser-playwright 4.1.0, @vitest/coverage-v8 4.1.0, happy-dom 20.8.4, storybook 10.2.19, tailwindcss 4.2.1, typescript-eslint 8.57.1, vite 8.0.0 (MAJOR) Vite 8 compatibility: added build.cssMinify: 'esbuild' to work around lightningcss/tailwindcss interaction issue (tailwindlabs/tailwindcss#19789). Closes: #294, #295, #296, #297, #298, #299, #300, #301, #302, #303, #304, #275, #277 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…used dep Code-splitting with React.lazy + Suspense: - 13 views lazy-loaded; main bundle 1,807 KB → 516 KB (71% reduction) - recharts (361 KB) and framer-motion (133 KB) split into separate chunks - Views load on demand: ShotHistory, Settings, PourOver, LiveShot, etc. Shared framer-motion animation variants: - New lib/animations.ts with 6 variants (fadeIn, slideUp, scaleIn, slideInRight, collapse, staggerContainer) + 2 spring configs - Applied to StartView, ShotList, AdvancedCustomization - Spring physics replaces basic duration/easeOut timing Granular error boundaries: - New FeatureErrorBoundary component (compact inline card, not full-screen) - Wraps Settings, ShotHistory, LiveShot views - i18n keys added to all 6 locales - Root ErrorBoundary preserved as last-resort catch-all Remove unused @tanstack/react-query: - Zero usage in codebase; removed from dependencies Container queries: skipped — ResizablePanel components exist but are not used anywhere in the app; no viewport queries to convert. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR bumps the project to 2.3.0-beta.1 and introduces a set of new UX features around dial-in guidance, profile recommendations, shot history/detail UI, and AI analysis/recommendation application, with supporting backend endpoints and tests.
Changes:
- Add a new Dial-In Guide wizard (frontend + backend session persistence + recommendation endpoint).
- Add profile recommendations (from selected tags) and find similar profiles flows.
- Refactor/extend analysis parsing + UI to support structured sections and applying AI recommendations to machine profiles.
Reviewed changes
Copilot reviewed 74 out of 76 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSION | Bumps root version to 2.3.0-beta.1. |
| CLAUDE.md | Adds browser-testing.md to referenced skills. |
| AGENTS.md | Adds browser-testing.md to referenced skills. |
| .github/copilot-instructions.md | Adds browser-testing.md to referenced skills. |
| .github/CONVENTIONS.md | Adds explicit browser-testing-before-release gate. |
| .github/skills/release.md | Requires browser testing protocol before release bump. |
| .github/skills/browser-testing.md | New manual browser testing protocol document. |
| .github/workflows/tests.yml | Expands CI pytest list to include new backend tests. |
| apps/web/vite.config.ts | Build tweaks: CSS minifier workaround + chunk splitting. |
| apps/web/src/views/StartView.tsx | Adds “Dial-In Guide” navigation button and shared animation variants. |
| apps/web/src/views/FormView.tsx | Adds ProfileRecommendations block to the profile generation form. |
| apps/web/src/types/index.ts | Adds dial-in view state. |
| apps/web/src/lib/parseAnalysis.ts | Adds structured analysis + recommendations JSON parsing utilities. |
| apps/web/src/lib/parseAnalysis.test.ts | Adds unit tests for recommendation parsing helpers. |
| apps/web/src/lib/animations.ts | Adds shared framer-motion transitions/variants. |
| apps/web/src/components/VariableAdjustPanel.tsx | New variable override UI panel with sliders + reset behavior. |
| apps/web/src/components/VariableAdjustPanel.test.tsx | Tests for variable adjustment panel behavior. |
| apps/web/src/components/TasteCompassInput.test.tsx | Tests for Taste Compass input interactions. |
| apps/web/src/components/SkipNavigation.tsx | Localizes skip-nav link labels using i18n keys. |
| apps/web/src/components/ShotHistoryView/useReplayAnimation.ts | New hook to drive replay animation timing/state. |
| apps/web/src/components/ShotHistoryView/types.ts | New shared types/constants for shot history/detail views. |
| apps/web/src/components/ShotHistoryView/ShotList.tsx | New shot list UI with refresh/background refresh indicators. |
| apps/web/src/components/ShotHistoryView/ShotHistoryView.tsx | New top-level shot history view orchestrator (list/detail). |
| apps/web/src/components/ShotHistoryView/SearchingLoader.tsx | New progress-based loader component for shot searching/loading. |
| apps/web/src/components/ShotHistoryView/index.ts | Barrel export for ShotHistoryView. |
| apps/web/src/components/SectionCard.tsx | New reusable card renderer for parsed analysis sections. |
| apps/web/src/components/RecommendationSelectionDialog.tsx | New dialog to select/apply parsed AI recommendations. |
| apps/web/src/components/ProfileRecommendations.tsx | New tag-driven profile recommendation UI (debounced fetch + collapsible). |
| apps/web/src/components/ProfileCatalogueView.tsx | Adds aria-labels to icon-only action buttons. |
| apps/web/src/components/ProfileBreakdown.tsx | Adjusts validation: suppress emoji warnings for adjustable vars; refine unused warnings. |
| apps/web/src/components/ProfileBreakdown.test.tsx | Updates tests to reflect the new emoji warning behavior. |
| apps/web/src/components/MarkdownEditor.tsx | Improves header layout wrapping responsiveness. |
| apps/web/src/components/LlmAnalysisModal.tsx | Refactors to use shared parsing + SectionCard. |
| apps/web/src/components/HistoryView.tsx | Adds “Find Similar” overlay integration in profile detail view. |
| apps/web/src/components/FindSimilarOverlay.tsx | New overlay to fetch and present similar profiles. |
| apps/web/src/components/FeatureErrorBoundary.tsx | New feature-scoped error boundary component. |
| apps/web/src/components/ExpertAnalysisView.tsx | Adds recommendations parsing + “Apply Recommendations” flow. |
| apps/web/src/components/DialInWizard.tsx | New multi-step dial-in wizard container. |
| apps/web/src/components/DialInTasteStep.tsx | Wizard step for collecting taste compass input. |
| apps/web/src/components/DialInRecommendStep.tsx | Wizard step to fetch/show recommendations (AI or fallback). |
| apps/web/src/components/DialInProfileStep.tsx | Wizard step to select a starting machine profile or enter one manually. |
| apps/web/src/components/DialInPrepStep.tsx | Wizard prep checklist step. |
| apps/web/src/components/DialInHistoryStep.tsx | Wizard iteration history summary step. |
| apps/web/src/components/DialInCoffeeStep.tsx | Wizard coffee details capture step. |
| apps/web/src/components/DialInBrewStep.tsx | Wizard brew/shot completion step. |
| apps/web/src/components/charts/EspressoChart.tsx | Adds aria-label for chart accessibility. |
| apps/web/src/components/AdvancedCustomization.tsx | Switches to shared animation variants/transitions. |
| apps/web/public/locales/es/translation.json | Adds/extends i18n keys for recommendations, dial-in, a11y, etc. (ES). |
| apps/web/public/locales/en/translation.json | Adds/extends i18n keys for recommendations, dial-in, a11y, etc. (EN). |
| apps/web/public/locales/de/translation.json | Adds/extends i18n keys for recommendations, dial-in, a11y, etc. (DE). |
| apps/web/package.json | Bumps web version + updates dependency versions. |
| apps/server/test_taste_compass.py | New backend tests for taste compass prompt + caching helpers. |
| apps/server/test_recommendations.py | New backend tests for profile recommendation logic/service. |
| apps/server/services/temp_profile_service.py | Adds variable override application helper + new temp profile prefix. |
| apps/server/services/gemini_service.py | Adds taste-hash helper for cache differentiation. |
| apps/server/services/dialin_service.py | New dial-in session store with persistence, TTL pruning, and locking. |
| apps/server/requirements.txt | Updates pinned backend dependency versions. |
| apps/server/prompt_builder.py | Adds taste context prompt + dial-in recommendation prompt builder. |
| apps/server/models/dialin.py | New Pydantic models for dial-in sessions/iterations. |
| apps/server/main.py | Loads dial-in sessions on startup; registers dial-in router. |
| apps/server/api/routes/scheduling.py | Adds run-profile-with-overrides endpoint + save semantics. |
| apps/server/api/routes/profiles.py | Adds apply-recommendations endpoint + recommendation/similar endpoints; invalidates caches on profile changes. |
| apps/server/api/routes/dialin.py | Adds dial-in session/iteration endpoints + AI/rules recommendation generation. |
You can also share your feedback on Copilot code review. Take the survey.
| const SHOT_QUOTES = [ | ||
| { quote: "You Miss 100% of the Shots You Don't Take", author: "Wayne Gretzky", meta: "— 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" }, | ||
| { quote: "One shot, one opportunity", author: "Eminem" }, | ||
| { quote: "A shot in the dark", author: "Ozzy Osbourne" }, | ||
| { quote: "Shoot for the moon", author: "Les Brown" }, | ||
| ] |
There was a problem hiding this comment.
Addressed in 1914843. Added an i18n exemption comment explaining these are well-known cultural references that intentionally stay in their original language regardless of locale. This is a deliberate design decision per project owner.
| useEffect(() => { | ||
| if (tags.length < 2) { | ||
| setRecommendations([]) | ||
| return | ||
| } |
There was a problem hiding this comment.
Already fixed. The component uses abortRef (useRef) — line 44 aborts any in-flight request before starting a new one, and the cleanup effect at line 97 aborts on unmount. The early-return branch at tags.length < 2 is safe because any in-flight request from a previous render with ≥2 tags will be aborted by the next effect run's cleanup.
| try: | ||
| overrides_dict: dict = json.loads(overrides_json) | ||
| except (json.JSONDecodeError, TypeError) as exc: | ||
| raise HTTPException(status_code=422, detail=f"Invalid overrides JSON: {exc}") | ||
|
|
||
| if save_mode not in ("none", "save_original", "save_new"): | ||
| raise HTTPException(status_code=422, detail=f"Invalid save_mode: {save_mode}") | ||
|
|
||
| if save_mode == "save_new" and not new_name.strip(): | ||
| raise HTTPException(status_code=422, detail="new_name is required when save_mode is save_new") | ||
|
|
||
| # Reject info_ variable overrides | ||
| info_keys = [k for k in overrides_dict if k.startswith("info_")] | ||
| if info_keys: | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail=f"Cannot override info variables: {info_keys}", | ||
| ) |
There was a problem hiding this comment.
Already fixed. json.loads() at line 297 with try/except handles JSONDecodeError/TypeError. The parsed result is typed as dict and validated before use. The .startswith() call on keys is safe since JSON object keys are always strings.
| for rec in recs: | ||
| variable = rec.get("variable", "") | ||
| recommended_value = rec.get("recommended_value") | ||
| stage = rec.get("stage", "") | ||
|
|
||
| if recommended_value is None: | ||
| skipped.append({"variable": variable, "reason": "no recommended_value"}) | ||
| continue |
There was a problem hiding this comment.
Fixed in 1914843. Added isinstance(rec, dict) guard — non-dict entries are now skipped with reason "invalid entry (not an object)". Also added math.isfinite() validation for recommended_value before processing, rejecting NaN/Infinity with reason "invalid recommended_value".
| {adjustableVars.length === 0 && ( | ||
| <p className="text-sm text-muted-foreground text-center py-4"> | ||
| {t('variables.noAdjustable')} | ||
| </p> | ||
| )} |
There was a problem hiding this comment.
Fixed in 1914843. Removed the unreachable adjustableVars.length === 0 conditional block at the end of the render. The early return null at line 69 already handles this case.
| let cancelled = false | ||
| const fetchRecommendations = async () => { | ||
| try { | ||
| const serverUrl = await getServerUrl() | ||
| const res = await fetch(`${serverUrl}/api/dialin/sessions/${session.id}/recommend`, { | ||
| method: 'POST', | ||
| }) | ||
|
|
||
| if (!res.ok) throw new Error(`HTTP ${res.status}`) | ||
|
|
||
| const data = await res.json() | ||
| if (!cancelled) { | ||
| setRecommendations(data.recommendations ?? []) | ||
| } |
There was a problem hiding this comment.
Already fixed. The catch/fallback block now uses localized t('dialIn.recommend.tips.*') strings for all rule-based recommendations (grindFiner, grindCoarser, increaseTemp, decreaseTemp, increaseDose, decreaseDose, lookingGood). The source field distinction is handled correctly — AI responses display server text, rule-based fallbacks use the local translated tips.
| toast.error( | ||
| err instanceof Error ? err.message : t("recommendations.applying") + " failed", | ||
| ); |
There was a problem hiding this comment.
Fixed in 1914843. Replaced t('recommendations.applying') + ' failed' with a dedicated t('recommendations.applyFailed') key. Added the key to all 6 locales with proper translations. Also performed a systemic sweep — replaced 50+ similar hardcoded error strings across 15+ files with t() calls.
| if (!res.ok) { | ||
| const body = await res.json().catch(() => ({})); | ||
| throw new Error(body.detail?.message || body.detail || "Failed to apply recommendations"); | ||
| } |
There was a problem hiding this comment.
Fixed in 1914843. Changed "Failed to apply recommendations" fallback in the throw to t('recommendations.applyFailed'). Added t to the useCallback dependency array. The key is translated in all 6 locales.
| "@radix-ui/react-toggle-group": "^1.1.2", | ||
| "@radix-ui/react-tooltip": "^1.1.8", | ||
| "@tailwindcss/container-queries": "^0.1.1", | ||
| "@tailwindcss/vite": "^4.1.11", | ||
| "@tanstack/react-query": "^5.83.1", | ||
| "@tailwindcss/vite": "^4.2.1", | ||
| "canvas-confetti": "^1.9.4", |
There was a problem hiding this comment.
Already committed. bun.lock (185 KB) is tracked and present in the branch. It was committed alongside the dependency updates.
The coverage-v8 plugin requires BaseCoverageProvider from vitest/node, which was added in vitest 4.1.0. The previous vitest@4.0.18 didn't export it, causing test:coverage to crash in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace save→load→delete temp profile pattern with ephemeral loading via POST /api/v1/profile/load. Profiles are loaded into machine memory without persisting to the catalogue, so: - Shot history shows the original profile name (not 'MeticAI Override: X') - No orphan profiles if cleanup fails - No save/delete roundtrips needed Changes: - Add async_load_profile_from_json() to meticulous_service.py - Add load_ephemeral() to temp_profile_service.py with state tracking - Refactor run-profile-with-overrides endpoint to use ephemeral load - Refactor pour-over prepare + recipe endpoints to use ephemeral load - Update cleanup/force_cleanup to skip delete for ephemeral profiles - Fix variable panel: derive from profile list data instead of separate fetch that could fail silently; add frontend-side variable synthesis - Update all related tests (820 backend + 36 recommendation + 320 FE pass) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added doseGrams and brewRatio to _MODE_DEFAULTS so save_preferences() no longer silently drops them. Fixes ratio mode not remembering dose and ratio across page refresh (free mode appeared to work by luck).
Add POST /api/machine/profiles/bulk-delete backend endpoint and BulkDeleteDialog frontend component. Shows non-catalogue profiles with select-all, confirmation, and progress feedback. i18n keys added for all 6 locales (en, sv, de, fr, es, it).
Enrich selectedProfile with full data (variables, temperature, final_weight) from the profile list once it loads. Previously the panel was invisible because the initial profile only had id+name.
The preheat endpoint now accepts an optional profile_id in the request body. When provided, the profile is loaded on the machine before starting the preheat cycle so the display shows the correct profile during heating. Previously the profile only loaded when the scheduled shot fired after preheat completed, causing confusion about which profile would run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Watchtower polls ghcr.io and replaces the local build with the published remote image. The dev overlay now sets watchtower.enable=false so local builds are not overwritten.
…mbnails - Add ProfileImage component with Coffee icon fallback - Integrate useProfileImageCache hook for async image loading - Simplify card layout with consistent avatar display - Add aria-label for back button accessibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…reheat nav - Fix intermittent missing variable panel (depend on selectedProfile not ID) - Translate synthesized variable names (Final Weight, Temperature) via i18n - Add save-as-new toggle in Options card when overrides exist - Navigate to live view after preheat started - Skip post-run save dialog when save-as-new is already enabled Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use responsive gap and padding (gap-2 sm:gap-3, p-2 sm:p-3) - Add break-words to dialog description for long profile names - Add shrink-0 to checkbox container to prevent squishing - Use smaller font on mobile for variable names (text-xs sm:text-sm) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add aria-label to 7 CaretLeft back buttons across 4 components (ShotAnalysisView, ProfileCatalogueView, HistoryView, ShotDetail) - Add role="img" + aria-label to ReplayChart, CompareChart, AnalyzeChart - Add 3 new chart a11y i18n keys (extractionReplay, extractionComparison, shotVsProfile) to all 6 locales - Add RunShotView i18n keys (saveAsNew, synthesized variables) to all locales Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…odel The ModePreferences Pydantic model was missing doseGrams and brewRatio fields. model_dump() silently stripped them on every PUT request, so values were always written back as null regardless of user input. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r results Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In the default view, profile cards now show no action buttons for a cleaner look. An Edit toggle in the header enables edit mode which reveals export-JSON, rename, and delete buttons on every card. The bulk-delete button also only appears in edit mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix preheat subtitle showing when preheat is disabled - Group final_weight and temperature into a Base Variables section - Add unused variable warning with amber highlight - Replace caret icons with +/- for expand/collapse Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ed warning - Exclude variables whose name starts with emoji from adjust panel - Exempt final_weight/temperature from unused variable detection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add Save Image button in lightbox to download profile image as PNG - Auto-scroll to image section when using Upload/Generate from lightbox - Strengthen no-text directive at start of image generation prompt - Add saveImage translation key to all 6 locales Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add useScrollToTop hook that resets window scroll when dependencies change (skipping initial mount to avoid double-scroll with App.tsx). Applied to: - ShotHistoryView: ShotList ↔ ShotDetail transitions - ShotDetail: opening/closing ExpertAnalysisView (AI analysis) - DialInWizard: step transitions Also remove unused GearSix import from HistoryView. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Power data is not useful in the live view graph. Remove showPower from all three EspressoChart instances (placeholder, live, complete). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…o catalogue When save_mode is save_new, persist the new profile to the machine catalogue BEFORE ephemeral load so: (1) the machine broadcasts the new name via WebSocket, (2) the image endpoint can find the profile, and (3) the profile appears in the catalogue immediately. Also update the toast to show the new profile name instead of the original when save-as-new is active. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both v3.2.0.2 and v3.2.0.3 return HTTP 404 from GitHub releases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two bugs fixed:
a) Preheat-only no longer navigates to live view. The onNavigateToLive
call is now inside the 'if (selectedProfile)' block so it only
fires when preheat + profile or run-only (not preheat-only).
b) Control center now shows the correct profile immediately after
selection. Added onProfileSelected callback from RunShotView that
optimistically patches machineState.active_profile before the
MQTT round-trip completes. useWebSocket now returns { state,
patchState } so App.tsx can pass patchState-based callbacks down.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Loading a profile into the machine before sending the preheat action causes the Meticulous machine to auto-start extraction once it reaches temperature (~30 seconds). This made the scheduled 10-minute delay meaningless — the shot would fire immediately after preheat. Fix: never send profile_id to the preheat endpoint. The scheduled task will load the profile at the correct time (after the preheat duration). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nch trigger Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comprehensive sweep of all user-facing error/failure messages across 15+ components and hooks. Added ~35 new error translation keys to all 6 locales (en/sv/de/es/fr/it). Also addresses PR #285 review comments: - #16: Added i18n exemption comment for SHOT_QUOTES (cultural refs) - #19: Added per-item dict validation in apply-recommendations endpoint - #20: Removed unreachable dead branch in VariableAdjustPanel - #22: Replaced hardcoded ' failed' concat with dedicated i18n key - #23: Replaced hardcoded 'Failed to apply recommendations' with i18n Files changed: ExpertAnalysisView, RecommendationSelectionDialog, FindSimilarOverlay, ProfileRecommendations, ProfileCatalogueView, ProfileImportDialog, SyncReport, SettingsView, RunShotView, ShotAnalysisView, PourOverView, VariableAdjustPanel, SearchingLoader, useHistory, useShotHistory, useUpdateStatus, useUpdateTrigger, profiles.py, + all 6 locale translation.json files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MeticAI v2.3.0 — Brewing Coach & Guided Experience
What's New
☕ Espresso Compass: After pulling a shot, tell MeticAI how it tasted (sour/bitter, weak/strong) and get suggestions for your next attempt — based on Barista Hustle's Espresso Compass, adapted to Meticulous.
🎛️ Tweak variables before brewing: Adjust dose, temperature, and all other variables on the fly without changing the saved profile.
🔍 Find Similar profiles: Browse your catalogue and discover related profiles based on tag matching.
📝 Actionable shot analysis: Analysis now gives you specific, selectable recommendations you can apply directly to your profile.
🖼️ Profile images: Download images and better image generation.
🧹 Catalogue edit mode: Cleaner default view. Tap Edit to export, rename, or bulk-delete profiles.
⚡ 71% smaller initial load: App loads much faster thanks to code-splitting.
♿ Accessibility improvements: Keyboard navigation, screen reader support, and reduced-motion throughout.
🐛 Plus a bunch of bug fixes (pour-over persistence, preheat, run shot improvements, mobile layout, etc.)
Full Changelog (since v2.2.2)
Features
Fixes
Refactors
Installation
Or upgrade:
curl -fsSL https://raw.githubusercontent.com/hessius/MeticAI/main/update.sh | bash