Skip to content

feat: MeticAI v2.3.0 — Brewing Coach & Guided Experience#285

Merged
hessius merged 57 commits intomainfrom
version/2.3.0
Mar 25, 2026
Merged

feat: MeticAI v2.3.0 — Brewing Coach & Guided Experience#285
hessius merged 57 commits intomainfrom
version/2.3.0

Conversation

@hessius
Copy link
Copy Markdown
Owner

@hessius hessius commented Mar 13, 2026

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

  • Preheat: don't load profile before preheat to prevent machine auto-start
  • Preheat-only navigation stays on Run/Schedule page (no erroneous live view nav)
  • Control center shows newly selected profile immediately (not stale)
  • Save-as-new shows correct name in live view and saves to catalogue
  • Variable panel reliability, i18n, save-as-new toggle, preheat subtitle
  • Pour-over: persist doseGrams and brewRatio across sessions
  • Catalogue UI cleanup with image cache and profile thumbnails
  • Move export/delete buttons to edit mode in catalogue
  • Scroll to top on all intra-component view transitions
  • Remove Power line from live shot chart
  • Mobile layout improvements for recommendation dialog
  • 50+ hardcoded error strings replaced with i18n t() calls
  • 15 pre-existing missing i18n keys added to non-English locales
  • s6-overlay bumped to 3.2.2.0 (3.2.0.x releases return 404)
  • CI: add security-events permission for SBOM generation

Refactors


Installation

docker pull ghcr.io/hessius/meticai:latest

Or upgrade:

curl -fsSL https://raw.githubusercontent.com/hessius/MeticAI/main/update.sh | bash

hessius and others added 9 commits March 12, 2026 23:55
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>
@hessius hessius added this to the 2.3 milestone Mar 13, 2026
Copilot AI review requested due to automatic review settings March 13, 2026 01:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR 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>
Copy link
Copy Markdown
Owner Author

@hessius hessius left a comment

Choose a reason for hiding this comment

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

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>
@hessius
Copy link
Copy Markdown
Owner Author

hessius commented Mar 13, 2026

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 & Fixed

i18n 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

Pattern Status
All fetch() calls use getServerUrl() ✅ (only ExpertAnalysisView was wrong, fixed)
All icon-only buttons have aria-label ✅ (fixed DialInProfileStep + ProfileCatalogue)
No dangerouslySetInnerHTML / XSS vectors
No any types in new TypeScript files ✅ (all properly typed)
All new endpoints dual-registered ✅ (9 endpoint pairs verified)
asyncio.Lock lazy pattern used ✅ (dialin_service follows scheduling_state pattern)
No unused imports
Cache invalidation on profile changes ✅ (profiles.py correctly invalidates)
useEffect dependencies correct
useCallback dependencies correct
Error handling on all fetch calls ✅ (fixed RecommendationSelectionDialog)
Session bounds / memory limits ✅ (added MAX_SESSIONS=200, 7-day TTL)
dialin_service logging convention ✅ (switched to get_logger())

Risk Assessment

Low risk items:

  • TasteFeedback.notes field exists in backend model but no frontend UI sends it — this is intentional future-proofing for when notes input is added
  • dialin.py routes use simple string error details vs structured format — consistent with existing route conventions in this codebase

Final State

  • Backend: 791 tests ✅
  • Frontend: 312 tests (23 files) ✅
  • Lint: 0 errors (16 pre-existing warnings) ✅
  • Build: Clean ✅
  • i18n: 1031 keys × 6 locales = full parity ✅
  • CI: test_taste_compass.py + test_recommendations.py now included

- 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>
@hessius
Copy link
Copy Markdown
Owner Author

hessius commented Mar 13, 2026

Browser Testing Results — v2.3.0-beta.1

Tested against Docker container running e2841d3 on http://localhost:3550.

# Area Status Notes
1 Start Screen ✅ PASS All 7 buttons, version banner 2.3.0-beta.1, 35 profiles, dark/light toggle
2 Profile Catalogue ✅ PASS 35 profiles loaded with images, filter/sync buttons, card layout
3 Profile Detail ✅ PASS Inline edit icon, sections rendered, notes, variables sidebar
4 Shot History ✅ PASS Progress bar, shot list, profile-filtered view
5 Shot Detail ✅ PASS Extraction graph (recharts), tabs (Replay/Compare/Analyze), star rating
6 Shot Analysis ✅ PASS Summary, Shot vs Profile chart, stage breakdown, AI analysis button
7 Dial-In Guide ✅ PASS All 7 steps work end-to-end, progress bar, back navigation
8 Taste Compass ✅ PASS 2D SVG compass, draggable dot, 16 descriptor chips
9 AI Recommendations ✅ PASS Gemini-powered recommendations in Dial-In Step 6
10 Run / Schedule ✅ PASS Profile selection, preheat/schedule toggles
11 Settings ✅ PASS Language, Gemini key (configured), AI toggles, Machine IP
12 Accessibility ✅ PASS Skip nav links, landmarks, aria-labels, live regions
13 i18n ✅ PASS All views fully translated to Swedish

Notes

  • Profile selector in Dial-In Step 2 shows "No profiles found" — profiles may need explicit machine fetch for wizard context
  • Console warnings about recharts chart width/height being 0 (pre-existing, not blocking)
  • 405 console error when loading profile in Run/Schedule (pre-existing nginx route issue)

Code Review Fixes Applied

  • a8420f3 — taste_x/taste_y bounds validation, descriptor validation, is_patchable default fix, AI-powered dial-in recommendations endpoint, DialInRecommendStep API integration
  • e2841d3 — Pre-release browser testing protocol documentation

CI Note

CI has not triggered on recent pushes — possibly GitHub Actions minutes quota. All tests pass locally: 852 backend + 312 frontend, lint clean, build succeeds.

hessius and others added 4 commits March 16, 2026 23:39
…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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR 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.

Comment on lines +6 to +14
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" },
]
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment on lines +79 to +83
useEffect(() => {
if (tags.length < 2) {
setRecommendations([])
return
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment on lines +296 to +313
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}",
)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment on lines +3398 to +3405
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
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 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".

Comment on lines +244 to +248
{adjustableVars.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">
{t('variables.noAdjustable')}
</p>
)}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 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.

Comment on lines +56 to +69
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 ?? [])
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment on lines +114 to +116
toast.error(
err instanceof Error ? err.message : t("recommendations.applying") + " failed",
);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 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.

Comment on lines +65 to +68
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.detail?.message || body.detail || "Failed to apply recommendations");
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 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.

Comment on lines 53 to 57
"@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",
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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>
hessius and others added 21 commits March 20, 2026 09:23
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>
hessius and others added 5 commits March 23, 2026 23:26
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>
@hessius hessius merged commit c7ae45f into main Mar 25, 2026
8 checks passed
@hessius hessius deleted the version/2.3.0 branch March 25, 2026 13:57
hessius added a commit that referenced this pull request Mar 25, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants