Skip to content

v2.2.0: MachineService, Machine Auto-detect, Profile Catalogue, Shot Annotations, Profile Notes, AI Controls#262

Open
hessius wants to merge 39 commits intomainfrom
version/2.2.0
Open

v2.2.0: MachineService, Machine Auto-detect, Profile Catalogue, Shot Annotations, Profile Notes, AI Controls#262
hessius wants to merge 39 commits intomainfrom
version/2.2.0

Conversation

@hessius
Copy link
Owner

@hessius hessius commented Mar 9, 2026

Summary

v2.2.0 implements all 10 milestone features plus comprehensive dependency upgrades, code review hardening, and zero deferred tech debt. This is a major release adding profile management, shot annotations, machine discovery, profile editing, shot analysis, and profile sync capabilities.

Milestone Issues — All Complete ✅

Commit Phase Issues Status
5042490 Phase 0 #252 - MachineService abstraction layer
20d42f1 Phase 0b #188 - Lazy-load canvas-confetti
17f0c75 Phase 1 #216 - Machine auto-detect via mDNS/zeroconf
1feb26e + 62edfab Phase 2 #192 - Profile catalogue management
f9cbdc1 Phase 2b #182 - Profile sync system
288d55c + dd298bf Phase 3 #179 - Shot annotations (stars + comments)
8337c56 Phase 4 #225 - Profile notes with markdown editor
118f166 Phase 4b #234 - AI summary controls
3642f41 Phase 5 #257 - Profile editing
40477e8 Phase 6 #259 - Shot Analysis view

Code Review — All Threads Resolved ✅

Category Count Details
Already addressed 4 getServerUrl() properly awaited (×2), threading.Lock present, analysis_service schema reworked
Outdated (code changed) 7 connectRef (×2), ControlCenter render, asyncio.get_event_loop, INTEGRATION_TESTING.md, backend.md Python version, VERSION
Intentional pattern 1 Dual route registration (established convention)
Fixed in beta.2 5 i18n error strings in OrphanResolutionDialog (4) + DeleteProfileDialog (4), annotation test coverage verified, restore endpoint verified

Code Hardening (beta.2)

  • fix(profiles): Added threading.Lock to history read-modify-write in profile rename cascade to prevent TOCTOU race condition
  • fix(shots): Bounded recent shots cache to 50 entries with TTL-based eviction and clamped cache key parameters to prevent unbounded memory growth

Dependency Upgrades — All 15 Dependabot PRs Addressed ✅

GitHub Actions

npm (apps/web)

Python

  • zeroconf==0.134.0 (new)

Test Results

  • Frontend: 277 tests passing (20 test files)
  • Backend: 741 tests passing
  • Build: TypeScript compiles clean
  • Lint: 0 errors (15 warnings)
  • E2E: Playwright passing
  • Docker: Container builds and starts successfully

Related Issues

Closes #252, #188, #216, #225, #234, #192, #179, #182, #257, #259
Tracking: #254

Dependabot PRs addressed: #264, #265, #266, #267, #268, #269, #270, #271, #272, #273, #274, #275, #276, #277, #278

Status: v2.2.0-beta.2 — Ready for User Testing

Copy link
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

Implements the v2.2.0 milestone feature set: new MachineService abstraction + UI features (AI controls, annotations/notes), plus backend endpoints for discovery, pour-over/recipes, SSE progress, and validation fallbacks.

Changes:

  • Added frontend preference plumbing (AI toggles), new markdown editing components, and shot/profile note/annotation UI.
  • Added backend services + routes for machine auto-detect, shot annotations, history notes, pour-over integration, recipes, and generation progress SSE.
  • Updated tooling/config (eslint hooks rules, dependencies, Docker build defaults syncing, CI tags), plus added Playwright E2E coverage.

Reviewed changes

Copilot reviewed 110 out of 147 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
apps/web/src/lib/aiPreferences.ts Adds localStorage-backed AI preferences and change event dispatching.
apps/web/src/index.css Adds “success glow” animation utility class.
apps/web/src/hooks/useWebSocket.ts Adjusts reconnect logic to use a ref-stored connect function.
apps/web/src/hooks/useThemePreference.ts Reworks “mounted” logic using useSyncExternalStore.
apps/web/src/hooks/useMachineService.ts New hook for accessing MachineService via context.
apps/web/src/hooks/useHistory.ts Extends HistoryEntry DTO with optional notes fields.
apps/web/src/hooks/useGenerationProgress.ts Adds SSE hook for profile generation progress.
apps/web/src/hooks/use-mobile.ts Migrates mobile breakpoint detection to useSyncExternalStore.
apps/web/src/hooks/use-desktop.ts Migrates desktop breakpoint detection to useSyncExternalStore + SSR snapshot.
apps/web/src/hooks/index.ts Re-exports generation progress hook/types/utilities.
apps/web/src/components/ui/sidebar.tsx Stabilizes skeleton width using useState initializer.
apps/web/src/components/ui/resizable.tsx Updates resizable UI wrapper API + lucide icon import path.
apps/web/src/components/ui/carousel.tsx Defers setApi callback to microtask; adds lint suppression.
apps/web/src/components/ShotHistoryView.tsx Adds shot annotations section + AI availability gating + auto-analyze preference.
apps/web/src/components/ShotAnnotation.tsx New shot annotation fetch/save component with MarkdownEditor.
apps/web/src/components/ProfileImportDialog.tsx Adds “generate AI descriptions” toggle gated by AI availability.
apps/web/src/components/ProfileBreakdown.tsx Tightens variable typing in summary/value building.
apps/web/src/components/MarkdownText.tsx Adjusts React key computation for trailing inline text segment.
apps/web/src/components/MarkdownEditor.tsx New reusable edit/preview markdown editor.
apps/web/src/components/LiveShotView.tsx Migrates machine commands to MachineService; adds clickable metric tile (tare).
apps/web/src/components/ControlCenterExpanded.tsx Migrates machine commands to MachineService; filters temp profiles.
apps/web/src/components/ControlCenter.tsx Migrates machine commands to MachineService; lazy-loads confetti; filters temp profiles.
apps/web/src/components/BetaBanner.tsx Adds beta banner driven by /api/version + session dismiss.
apps/web/src/components/AdvancedCustomization.tsx Adds “detailedKnowledge” toggle with warning UI.
apps/web/package.json Bumps web version + updates eslint/react-hooks deps + adds testing-library/dom.
apps/web/eslint.config.js Enables additional react-hooks v7 rules as errors.
apps/web/e2e/shot-history.spec.ts Adds E2E coverage around Run/Schedule navigation & scheduling UI.
apps/web/e2e/settings.spec.ts Adds E2E coverage for Settings navigation/sections.
apps/web/e2e/qr-code.spec.ts Makes QR E2E assertions resilient to non-localhost URLs.
apps/web/e2e/profile-generation.spec.ts Adds E2E coverage for generation form interactions + navigation.
apps/web/e2e/pour-over.spec.ts Adds E2E coverage for pour-over view (Docker-only).
apps/web/e2e/live-shot.spec.ts Adds E2E coverage for Live Shot view navigation (Docker-only).
apps/web/e2e/history.spec.ts Adds E2E coverage for history/catalogue navigation + console/network error detection.
apps/web/e2e/control-center.spec.ts Adds E2E coverage for Control Center (Docker-only).
apps/web/e2e/app.spec.ts Makes E2E tests skip when AI features are disabled in CI.
apps/web/e2e/api-integration.spec.ts Adds browser-driven API integration coverage (Docker-only).
apps/server/services/validation_service.py Adds schema-aware (or basic) profile validation layer with MCP fallback.
apps/server/services/shot_annotations_service.py Adds JSON-backed shot annotations persistence and caching.
apps/server/services/settings_service.py Adds betaChannel field to settings defaults.
apps/server/services/recipe_adapter.py Adds adapter from OPOS recipe JSON → machine profile stages.
apps/server/services/pour_over_preferences.py Adds persistence for pour-over UI mode preferences.
apps/server/services/pour_over_adapter.py Adds template-based pour-over profile adaptation.
apps/server/services/machine_discovery_service.py Adds mDNS + hostname machine auto-detect and verify helper.
apps/server/services/history_service.py Adds history entry note update + lookup by id.
apps/server/services/generation_progress.py Adds in-memory generation progress state + SSE streaming support.
apps/server/services/gemini_service.py Adds distilled knowledge string + AI-availability helper; clarifies error parsing docs.
apps/server/services/analysis_service.py Adds static profile description fallback when AI unavailable/fails.
apps/server/requirements.txt Adds SSE + zeroconf dependencies.
apps/server/requirements-test.txt Adds optional paho-mqtt for integration tests.
apps/server/main.py Syncs bundled defaults at startup; registers new routers; cleans stale temp profiles.
apps/server/conftest_integration.py Adds opt-in integration test fixtures + helper utilities.
apps/server/conftest.py Resets additional caches; adds autouse mocks/reset for validation + generation progress.
apps/server/api/routes/shots.py Adds improved machine-unreachable handling + shot annotation endpoints + AI unavailable error mapping.
apps/server/api/routes/recipes.py Adds recipe list/get endpoints.
apps/server/api/routes/pour_over.py Adds pour-over prepare/cleanup/active endpoints + preferences endpoints.
apps/server/api/routes/history.py Adds history notes GET/PATCH endpoints.
apps/server/api/routes/commands.py Adds POST /api/machine/detect endpoint for auto-detect.
apps/server/INTEGRATION_TESTING.md Documents how to run opt-in integration tests against real hardware.
apps/mcp-server Updates submodule pointer.
WINDOWS.md Adds installer troubleshooting guidance for PowerShell execution behavior.
VERSION Bumps project version string.
README.md Documents addon management post-install.
GEMINI.md Adds Continuity auto-generated project memory file.
CLAUDE.md Adds Continuity auto-generated project memory file.
AGENTS.md Adds Continuity auto-generated project memory file.
.vscode/settings.json Removes workspace-specific Python test runner settings.
.github/workflows/build-publish.yml Adjusts Docker tag rules for latest/beta behavior.
.github/skills/workflow.md Adds agent workflow documentation file.
.github/skills/testing.md Adds testing/build verification documentation file.
.github/skills/frontend.md Adds frontend standards documentation file.
.github/skills/backend.md Adds backend standards documentation file.
.github/prompts/write-plan.prompt.md Adds plan-writing prompt/skill definition file.
.github/prompts/worktree.prompt.md Adds worktree management prompt/skill definition file.
.github/prompts/verify.prompt.md Adds verification-before-completion prompt/skill definition file.
.github/prompts/superpowers.prompt.md Adds skill system usage prompt/skill definition file.
.github/prompts/review.prompt.md Adds requesting-code-review prompt/skill definition file.
.github/prompts/receive-review.prompt.md Adds receiving-code-review prompt/skill definition file.
.github/prompts/finish-branch.prompt.md Adds finishing-a-development-branch prompt/skill definition file.
.github/prompts/execute-plan.prompt.md Adds executing-plans prompt/skill definition file.
.github/prompts/dispatch-agents.prompt.md Adds parallel agent dispatch prompt/skill definition file.
.github/prompts/brainstorm.prompt.md Adds brainstorming prompt/skill definition file.
.dockerignore Ensures pour-over defaults (template/recipes) are included in Docker build context.
.cursorrules Adds Continuity auto-generated project memory file.

You can also share your feedback on Copilot code review. Take the survey.

@hessius
Copy link
Owner Author

hessius commented Mar 9, 2026

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Contributor

Copilot AI commented Mar 9, 2026

@hessius I've opened a new pull request, #263, to work on those changes. Once the pull request is ready, I'll request review from you.

@hessius hessius added this to the 2.2 milestone Mar 9, 2026
@hessius
Copy link
Owner Author

hessius commented Mar 9, 2026

🤖 CI is green (6/6 jobs). Requesting fresh Copilot code review after rebase + feature implementation.

@hessius hessius requested a review from Copilot March 10, 2026 11:27
Copy link
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

Copilot reviewed 92 out of 155 changed files in this pull request and generated 10 comments.


You can also share your feedback on Copilot code review. Take the survey.

hessius and others added 11 commits March 10, 2026 14:24
- Create MachineService interface with brewing, machine, and config commands
- Implement MeticAIAdapter that delegates to REST API
- Add MachineServiceContext and useMachineService hook
- Migrate LiveShotView, ControlCenter, ControlCenterExpanded, PourOverView
- Add mock for useMachineService in PourOverView tests
- All 277 tests pass
- Convert static import to dynamic import()
- Confetti chunk split out to separate ~11KB file
- Main bundle reduced by ~11KB
- Module only loaded when 100th shot milestone reached
- Add zeroconf>=0.134.0 dependency for mDNS service discovery
- Create machine_discovery_service.py with multi-tier discovery:
  - mDNS browse for _meticulous._tcp.local.
  - Hostname resolution for meticulous.local
  - Returns guidance when machine not found
- Add POST /api/machine/detect endpoint in commands.py
- Add 'Detect' button in SettingsView with spinner and result feedback
- Auto-fills IP field when machine is discovered
- Add English translation strings for detect/machineFound
- Add DELETE /api/machine/profile/{id} endpoint for profile deletion
- Add PATCH /api/machine/profile/{id} endpoint for profile renaming
- Create ProfileCatalogueView component with:
  - List all profiles from machine with metadata
  - Rename profiles inline with confirmation
  - Delete profiles with confirmation dialog
  - Export profile JSON to file
  - Show 'in history' badge for imported profiles
- Add Profile Management section in Settings with link to catalogue
- Add translation strings for profile catalogue UI
- Add 'profile-catalogue' view state type
- Create shot_annotations_service.py for persistent annotation storage
- Add GET/PATCH /api/shots/{date}/{filename}/annotation endpoints
- Create MarkdownEditor component with edit/preview toggle
- Create ShotAnnotation component for inline editing with auto-save
- Add annotation section to ShotHistoryView shot detail view
- Store annotations in data/shot_annotations.json keyed by date/filename
- Add translation strings for annotation UI
- Add notes and notes_updated_at fields to HistoryEntry interface
- Add update_entry_notes() and get_entry_by_id() to history_service.py
- Add GET/PATCH /api/history/{entry_id}/notes endpoints
- Add MarkdownEditor to ProfileDetailView for inline note editing
- Reuse existing MarkdownEditor component from shot annotations
- Add English translation strings for notes UI
- Add autoAnalyzeShots preference to control auto-analysis on shot view
- Add showAiInHistory preference to control AI summary visibility
- Add new toggle switches in Settings view for both preferences
- Update ShotHistoryView to respect autoAnalyzeShots setting
- Update HistoryView to respect showAiInHistory for coffee_analysis
- Add English translation strings for new AI controls
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
hessius and others added 9 commits March 10, 2026 14:24
- Add GET /api/shots/recent and /api/shots/recent/by-profile endpoints
- Create ShotAnalysisView with Recent and By Profile tabs
- Add Shot Analysis button to home page
- Fix ESLint errors (unused imports, dependency arrays)
- Add i18n keys to all 6 locales

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace deprecated asyncio.get_event_loop() with get_running_loop()
  in machine_discovery_service.py
- Add threading.Lock to shot_annotations_service.py to prevent
  concurrent read/modify/write race conditions on annotation cache

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GitHub Actions:
- docker/login-action v3 → v4
- docker/setup-buildx-action v3 → v4
- docker/setup-qemu-action v3 → v4
- docker/metadata-action v5 → v6
- docker/build-push-action v6 → v7

npm (apps/web):
- lucide-react ^0.484.0 → ^0.577.0
- Regenerated lockfile (picks up storybook, lodash, rollup patches)

Addresses dependabot PRs: #264, #265, #266, #267, #268, #273, #276, #277, #278

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build and all 277 tests pass clean.
Addresses dependabot PR #272.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build and all 277 tests pass. Addresses dependabot PR #269.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Not actively imported yet — bump is safe. Build + 277 tests pass.
Addresses dependabot PR #275.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Major version upgrade. Build + 277 tests pass. Bundle size actually
decreased slightly (1730 kB vs 1757 kB).
Addresses dependabot PR #274.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Major version upgrade. Zod 4 has backwards-compatible z.* API.
Build + 277 tests pass. Addresses dependabot PR #271.

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

Added missing translation keys to all 6 locale files (en, es, de, fr, it, sv)
that were causing raw i18n keys to display on the home screen.
hessius and others added 7 commits March 10, 2026 17:11
- edit_profile: return full profile_dict instead of 5-field subset
  (v2.3 #258 apply-recommendations needs complete profile after edit)
- list_machine_profiles: include stages and variables via deep_convert_to_dict
  (v2.3 #95 profile recommendation scoring needs actual parameters)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HistoryView passed handleSaveNotes (API call + toast) as onChange to
MarkdownEditor, firing on every character. Separated onChange (local
state update) from onSave (explicit save button). Also fixed isSaving
→ saving prop name to match MarkdownEditor's interface.
…o fallback

- Changed validation_service to use ValidationLevel.STRICT (was SAFETY),
  enforcing emoji naming and other strict rules.
- Added recursive unused-variable detection to _basic_validate() fallback
  so profiles are checked even when the full validator isn't available.
- Updated mcp-server submodule with recursive variable scanning fix.
- Fix missing await on getServerUrl() in handleDetectMachine causing 405 error
- Fix Swedish 'Skottanalys' → 'Shot-analys' translation
- Add 9 missing settings keys to all 5 non-English locales (de, es, fr, it, sv):
  autoAnalyzeShots, autoAnalyzeShotsDescription, showAiInHistory,
  showAiInHistoryDescription, detect, machineFound, profileManagement,
  viewProfileCatalogue, profileCatalogueDescription
- Fix target curves not rendering after recharts v3 migration by replacing
  Customized component with TargetCurvesSvg using useXAxisScale/useYAxisScale hooks
- Fix shot click in ShotAnalysisView to navigate directly to specific shot
  instead of only opening the profile's shot list
- Add 5-minute cache with refresh button to ShotAnalysisView to avoid
  redundant API calls when switching between views
- Remove autoAnalyzeShots setting and auto-analysis behavior
  (shots are now analyzed only on manual request)
- Remove showAiInHistory setting (coffee analysis always visible)
- Remove both settings from aiPreferences.ts, SettingsView, and all 6 locales
- Consolidate profile catalogues: move machine profile management (gear icon
  with sync badge) into HistoryView header, remove from Settings
- ProfileCatalogueView now navigates back to profile catalogue, not start
- Show actual 1-5 star rating in shot history list (was just boolean indicator)
- Clean up unused translations (profileManagement, viewProfileCatalogue,
  profileCatalogueDescription, autoAnalyzeShots, showAiInHistory)

Refs: #234
- Add 'Generate AI Explanation' button on profile detail view for
  profiles with static (non-AI) descriptions. Detects static summaries
  by checking for 'generated without AI assistance' marker text.
- Add POST /api/profile/{entry_id}/regenerate-description backend endpoint
  that regenerates a profile description using AI and updates history.
- Add auto-sync toggle in Profile Catalogue that periodically (every 5 min)
  checks for new/updated profiles on the machine and imports them.
- Add POST /api/profiles/auto-sync backend endpoint for automatic import
  of new profiles and acceptance of updated ones.
- Add auto-sync preference persistence via localStorage.
- Add translations for all new UI text in all 6 locales.
- Fix unused ArrowsClockwise import lint error in HistoryView.

Closes #234
@hessius
Copy link
Owner Author

hessius commented Mar 10, 2026

v2.2.0 Manual Testing Checklist

Testing against locally built container (1acbbcf) connected to real Meticulous machine.

Issue #234 — Generate AI Explanation Button

  • Open a profile with a static (non-AI) description — "Generate AI Explanation" button is visible
  • Open a profile with an AI description — button is NOT visible
  • Click "Generate AI Explanation" — spinner shows, description replaces in-place on success
  • After regeneration, the button disappears (static marker text is gone)
  • With Gemini API key not configured — button should not show (aiConfigured is false)
  • Reload the page — regenerated description persists

Import Toggle (regression check)

  • Open "Add Profile" dialog — "Generate AI Descriptions" toggle is visible
  • Toggle OFF, import a profile — profile gets a static (non-AI) description
  • Toggle ON, import a profile — profile gets an AI-generated description (requires API key)
  • Bulk import from machine respects the toggle

Auto-sync

  • Navigate to Profile Catalogue (gear icon from History)
  • "Auto-sync profiles from machine" toggle is visible below the header
  • Toggle ON — immediate sync runs, toast shows if any profiles imported/updated
  • Toggle OFF — polling stops
  • Reload page — toggle state persists (stored in localStorage)

Profile Catalogue & Sync (regression)

  • Manual "Sync" button still works
  • Sync badge count displays correctly
  • Refresh button still works
  • SyncReport dialog opens and functions

Profile Detail View (regression)

  • Star ratings display correctly (1-5 filled stars)
  • Shot History & Analysis button works
  • Notes section saves/loads
  • Profile image displays correctly

Settings (regression)

  • Machine detection button works
  • AI toggle works
  • Language switching works

i18n Spot-check

  • Switch to Swedish — "Generera AI-förklaring" appears on static profiles
  • Switch to German — "KI-Erklärung generieren" appears
  • Auto-sync toggle label renders in selected language

🔄 Testing in progress...

@hessius
Copy link
Owner Author

hessius commented Mar 10, 2026

Manual QA Testing Results ✅

Tested against running Docker container (v2.2.0-beta.1) at localhost:3550, connected to live Meticulous machine (192.168.50.168, 23.3°C, idle).

PR #262 Feature Tests

Test Result Notes
Profile Catalogue sync view ✅ PASS Header "Profilkatalog", subtitle "31 profiler på din maskin", Synka button with badge(27), Uppdatera button, auto-sync toggle, orphan warning "7 profil(er)...", profile list with export/rename/delete, "I historiken" badges
Auto-sync toggle ON ✅ PASS Toggle ON → toast "Autosynk: 20 importerade, 0 uppdaterade", badge changed 27→7, buttons disabled during op, all imported profiles got "I historiken" badge
Auto-sync toggle OFF ✅ PASS Toggle OFF works, no errors
Generate AI Explanation (static profile) ✅ PASS "Pour Over 3" → button "Generera AI-förklaring" visible, static description shown. Click → toast "AI-förklaring genererad", description replaced with AI analysis, button disappeared
Generate AI Explanation (AI profile) ✅ PASS "Shock-olate Decadence" → button correctly NOT shown (AI-generated profile)
Import dialog AI toggle ✅ PASS "Lägg till profil" dialog shows import options + "Generera AI-beskrivningar" toggle (checked by default) with description "Lägger till rikare profilsammanfattningar under import"

Regression Tests

Test Result Notes
Shot-analys navigation ✅ PASS "Körningsanalys" view with tabs "Senaste"/"Per profil", star/notes icons visible
Shot detail view ✅ PASS Graph with 4 series + stage overlays, stats in Swedish (Varaktighet, Utbyte, Temp)
Star ratings (1-5) ✅ PASS "Tropic Like It's Hot" Mar 8 shot: 4 filled + 1 unfilled star (4/5), notes icon visible
Settings page i18n ✅ PASS Full Swedish rendering confirmed. Switched to Deutsch → all German translations rendered correctly ("Einstellungen", "KI-ASSISTENT", etc.). Switched back to Svenska without issue
Profile detail view ✅ PASS Description, preparation, profile details panel, personal notes, export options all render

Pre-existing Issues Found (NOT from this PR)

  1. Missing shotHistory translations in all 5 non-English locales — ~30 keys in the shotHistory section are still in English (title, shotDetails, searching, checkingNewShots, noShots, shotsCount, etc.). Only ~11 keys were translated previously.
  2. Hardcoded "Last updated:" string in ShotHistoryView.tsx:2605 — needs a translation key.

All 11 test items PASSED. Pre-existing i18n gaps logged to tasks.md for follow-up in this PR.

- Translated title, shotDetails, searching, scanningLogs, checkForNewShots,
  noShots, shotsCount, replay, compare, analyze, shotAnalysis, analyzeShot,
  shotSummary, weight, preinfusion, stageAnalysis, profileTarget, exitTriggers,
  limits, hitLimit, notReached, stageFailed, incomplete, exporting,
  exportAsImage, viewAiAnalysis, getAiAnalysis, runAnalysisForOverlay
  in sv/de/es/it/fr
- Added missing shotHistory.aiUnavailable to de/es/it/fr
- Added shotHistory.lastUpdated key to all 6 locales
- Replaced hardcoded 'Last updated:' in ShotHistoryView.tsx with t() call
@hessius
Copy link
Owner Author

hessius commented Mar 10, 2026

QA Checklist — Final Pass (ef9494c)

Tested against Docker container rebuilt from latest commit with --no-cache.

Feature: Generate AI Explanation (Issue #234)

  • Static profile shows "Generera AI-förklaring" button (tested: Slow Preinfusion) ✅
  • AI-generated profile does NOT show the button (tested: Shock-olate Decadence) ✅
  • Click button → toast notification → description replaced → button disappears (tested: Damian's LRv2) ✅
  • aiConfigured guard prevents button when Gemini API key not set — verified in source ✅
  • AI description persists after page reload ✅

Feature: Auto-Sync from Machine

  • Import dialog toggle visible and checked by default ✅
  • Import toggle OFF/ON works (all profiles already synced, "0 tillgänglig") ✅
  • ProfileCatalogueView shows header with profile count, Synka button (badge 7), Uppdatera button ✅
  • Auto-sync toggle "Synka profiler automatiskt från maskinen" — toggles ON/OFF, localStorage persisted ✅
  • SyncReport dialog shows 7 orphaned profiles with restore/delete buttons, AI toggle ✅

Profile Detail & Settings

  • Profile detail view name, date, image, description, shot history, notes, profile details, export — all present ✅
  • Settings all sections translated (Språk, AI-Assistent, Maskin-IP, MQTT, Utseende) ✅
  • Language switching Swedish → German → Swedish: all strings translated ✅

i18n

  • ~35 shotHistory keys translated in sv/de/es/it/fr ✅
  • "Last updated:" hardcoded string replaced with t('shotHistory.lastUpdated')

Tests & Build

  • Backend: 741 tests pass (pytest test_main.py -q) ✅
  • Frontend: 277 tests pass (bun run test:run) ✅
  • Build: bun run build clean ✅
  • Lint: 0 errors ✅

Copilot Review Comments — All 17 Unresolved Threads Responded To

Category Count Details
Already addressed 4 getServerUrl() properly awaited (×2), threading.Lock present, analysis_service schema reworked
Outdated (code changed) 7 connectRef (×2), ControlCenter render, asyncio.get_event_loop, INTEGRATION_TESTING.md, backend.md Python version, VERSION
Intentional pattern 1 Dual route registration (established convention)
Acknowledged for follow-up 5 Annotation test coverage (×2), non-i18n error strings in OrphanResolutionDialog + DeleteProfileDialog, missing restore endpoint

Known Follow-up Items (non-blocking)

  1. Non-i18n error strings in OrphanResolutionDialog.tsx (4 strings) and DeleteProfileDialog.tsx (3 strings) — error-path toast messages
  2. Missing restore endpoint POST /api/machine/profile/restore/{id} — frontend calls it but backend doesn't implement it yet
  3. Annotation test coverage — dedicated unit tests for shot annotation CRUD

- i18n: Replace 8 hardcoded English error strings in OrphanResolutionDialog
  and DeleteProfileDialog with t() translation keys across all 6 locales
- deps: Bump eslint ^10.0.2 → ^10.0.3 (PR #270), storybook ^10.2.7 → ^10.2.17 (PR #276)
- fix(profiles): Add threading.Lock to history read-modify-write in profile
  rename cascade to prevent TOCTOU race condition
- fix(shots): Bound recent shots cache to 50 entries with TTL-based eviction
  and clamped cache key parameters to prevent unbounded memory growth
- chore: Bump version to 2.2.0-beta.2

All tests pass: 741 backend, 277 frontend. Build clean. Lint: 0 errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hessius
Copy link
Owner Author

hessius commented Mar 11, 2026

v2.2.0-beta.2 — Code Review Hardening (19577a7)

CI: ✅ Test Suite green (6/6 jobs). Build and Publish in progress.

What changed in beta.2

1. All 5 unresolved review threads addressed:

  • ✅ i18n: Replaced 8 hardcoded English error strings in OrphanResolutionDialog.tsx and DeleteProfileDialog.tsx with t() translation keys + 30 translations across 6 locales (en/sv/de/es/fr/it)
  • ✅ Annotation test coverage verified — TestShotAnnotationEndpoints has 9 comprehensive tests
  • ✅ Restore endpoint verified — POST /api/machine/profile/restore/{history_id} exists in profiles.py:2721

2. Remaining dependabot PRs addressed:

3. Comprehensive code review — 2 bugs found and fixed:

  • TOCTOU race in profile rename cascade (profiles.py): History read-modify-write during profile rename had no lock protection. Concurrent renames could lose history updates. Fixed with threading.Lock.
  • Unbounded cache growth (shots.py): _recent_shots_cache could grow without limit via varied limit/offset parameters. Fixed with 50-entry cap, TTL-based eviction, and clamped cache key parameters.

4. Version bumped to 2.2.0-beta.2

Deferred items: None

All previously deferred items have been resolved. Zero tech debt carried forward.

Next: User testing on beta.2 → final 2.2.0 version bump → merge

@hessius hessius requested a review from Copilot March 11, 2026 13:00
Copy link
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

Copilot reviewed 52 out of 53 changed files in this pull request and generated 12 comments.

Comments suppressed due to low confidence (8)

apps/web/src/components/ShotHistoryView.tsx:1

  • This update path introduces undefined as never into a Record<string, {...}> and then immediately schedules a second state update to delete the key. Besides being type-unsafe, the double setAnnotationSummaries can cause unnecessary renders and makes state updates harder to reason about. Prefer a single functional update that either sets the summary or deletes the key (by cloning and delete), without writing undefined into the record.
    apps/web/src/components/ShotHistoryView.tsx:1
  • handleSelectShot, initialShotDate, and initialShotFilename are used inside this effect but omitted from the dependency list via an eslint disable. This can lead to stale closures if those props/functions change without a remount. A cleaner approach is to (1) make handleSelectShot stable via useCallback, (2) include the props in the dependency array, and (3) call the async handler as void handleSelectShot(target) to avoid unhandled promise warnings.
    apps/web/src/components/ShotHistoryView.tsx:1
  • handleSelectShot, initialShotDate, and initialShotFilename are used inside this effect but omitted from the dependency list via an eslint disable. This can lead to stale closures if those props/functions change without a remount. A cleaner approach is to (1) make handleSelectShot stable via useCallback, (2) include the props in the dependency array, and (3) call the async handler as void handleSelectShot(target) to avoid unhandled promise warnings.
    apps/web/src/components/ShotAnnotation.tsx:1
  • This is an optimistic UI update (setRating(newRating)) but failures don’t revert the rating, leaving the UI out of sync with the backend. Capture the previous rating before updating and restore it in the catch block if the PATCH fails.
    apps/web/src/components/SettingsView.tsx:1
  • The fallback guidance string is hard-coded English and bypasses i18n, and response.ok is not checked before attempting to parse JSON. Consider (1) checking response.ok and showing a translated error message, and (2) moving the fallback guidance into translations (e.g., t('settings.detectFailedGuidance')).
    apps/web/src/components/SettingsView.tsx:1
  • The fallback guidance string is hard-coded English and bypasses i18n, and response.ok is not checked before attempting to parse JSON. Consider (1) checking response.ok and showing a translated error message, and (2) moving the fallback guidance into translations (e.g., t('settings.detectFailedGuidance')).
    apps/web/src/components/SettingsView.tsx:1
  • The fallback guidance string is hard-coded English and bypasses i18n, and response.ok is not checked before attempting to parse JSON. Consider (1) checking response.ok and showing a translated error message, and (2) moving the fallback guidance into translations (e.g., t('settings.detectFailedGuidance')).
    apps/web/src/components/ShotAnalysisView.tsx:1
  • These <button> elements should set type="button" to avoid accidentally acting as submit buttons if this component is ever rendered inside a <form>.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +1014 to +1018
# Sort by timestamp descending (handle None timestamps)
all_shots.sort(
key=lambda s: float(s["timestamp"]) if s["timestamp"] else 0,
reverse=True,
)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

float(s["timestamp"]) can raise ValueError if the machine returns a non-numeric timestamp string (or an unexpected type), turning a single bad shot into a 500. Consider a safe parse helper that returns 0 on parse failure (and/or filters invalid timestamps).

Copilot uses AI. Check for mistakes.
Comment on lines +1037 to +1039
@router.get("/shots/recent/by-profile")
@router.get("/api/shots/recent/by-profile")
async def get_recent_shots_by_profile(request: Request, limit: int = 50, offset: int = 0):
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The endpoint accepts offset, but the implementation never applies it (it always groups from offset=0), so pagination semantics are incorrect and the cache key includes an offset that doesn’t actually affect the cached payload. Apply offset consistently (either page first then group, or define grouping-pagination rules explicitly and implement them) so responses match the query params.

Copilot uses AI. Check for mistakes.
Comment on lines +1053 to +1059
# Reuse the flat recent-shots logic
flat_response = await get_recent_shots(request, limit=limit + offset, offset=0)
flat_shots = flat_response["shots"]

# Group by profile_name
grouped: dict[str, dict] = {}
for shot in flat_shots:
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The endpoint accepts offset, but the implementation never applies it (it always groups from offset=0), so pagination semantics are incorrect and the cache key includes an offset that doesn’t actually affect the cached payload. Apply offset consistently (either page first then group, or define grouping-pagination rules explicitly and implement them) so responses match the query params.

Copilot uses AI. Check for mistakes.
Comment on lines +995 to +997
# We need enough shots for offset + limit; collect greedily
needed = offset + limit
for date in dates:
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Even though you stop once all_shots >= needed, within a date you still enqueue tasks for all shot files on that date. On dates with many shots, this can create a very large task list and wasted work. Consider slicing files down to the remaining number needed (e.g., needed - len(all_shots)) before creating tasks.

Copilot uses AI. Check for mistakes.
Comment on lines +1001 to +1004
files_result = await async_get_shot_files(date)
if hasattr(files_result, "error") and files_result.error:
continue
files = sorted([f.name for f in files_result], reverse=True) if files_result else []
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Even though you stop once all_shots >= needed, within a date you still enqueue tasks for all shot files on that date. On dates with many shots, this can create a very large task list and wasted work. Consider slicing files down to the remaining number needed (e.g., needed - len(all_shots)) before creating tasks.

Copilot uses AI. Check for mistakes.
Comment on lines +1151 to +1154
except ValueError as e:
raise HTTPException(status_code=422, detail={"status": "error", "error": str(e)})
except Exception as e:
logger.error(
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

await request.json() can raise a JSON decoding error for malformed bodies, which currently falls into the generic Exception handler and returns a 500. It should return a client error (typically 400/422) for invalid JSON. Add an explicit catch for JSON decode errors and raise HTTPException(status_code=400, ...) (or allow FastAPI’s 422 handling via a Pydantic model).

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +53
const handleSave = useCallback(() => {
onSave?.()
setIsEditing(false)
setShowPreview(false)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

If onSave performs async work (as in ShotAnnotation / history notes), the editor closes immediately, and users can re-open/edit while a save is still in-flight. Consider making handleSave async and awaiting onSave (or letting the parent control edit mode), only exiting edit mode after success (and keeping it open on failure).

Suggested change
const handleSave = useCallback(() => {
onSave?.()
setIsEditing(false)
setShowPreview(false)
const handleSave = useCallback(async () => {
if (!onSave) return
try {
await onSave()
setIsEditing(false)
setShowPreview(false)
} catch {
// Keep editor open on save failure; error handling is delegated to the caller
}

Copilot uses AI. Check for mistakes.
)


@router.get("/api/history/{entry_id}/notes")
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

New history notes endpoints are introduced here, but there are no corresponding tests in the added backend test suite (e.g., covering set/clear notes and 404 on missing entries). Adding tests would help prevent regressions in notes persistence and response shape.

Copilot uses AI. Check for mistakes.
}


@router.patch("/api/history/{entry_id}/notes")
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

New history notes endpoints are introduced here, but there are no corresponding tests in the added backend test suite (e.g., covering set/clear notes and 404 on missing entries). Adding tests would help prevent regressions in notes persistence and response shape.

Copilot uses AI. Check for mistakes.
Comment on lines +237 to +238
@router.post("/api/machine/detect")
async def detect_machine():
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The machine auto-detect endpoint is new, but there are no tests validating its response contract (e.g., found false includes guidance, found true includes ip/hostname/method/verified, and error handling). Adding a minimal test with mocked discovery/verify functions would lock down the API behavior expected by SettingsView.

Copilot uses AI. Check for mistakes.
hessius and others added 2 commits March 11, 2026 18:49
Add TestHistoryNotesEndpoints (5 tests):
- GET /api/history/{id}/notes success and 404
- PATCH /api/history/{id}/notes success, empty clear, and 404

Add TestMachineDetectEndpoint (4 tests):
- Machine found+verified, found+unverified, not found, exception

Addresses review threads 34-36 on PR #262.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
shots.py:
- Add _safe_float() helper for timestamp parsing (thread 25)
- Apply offset in by-profile endpoint before grouping (threads 26-27)
- Limit file enqueuing per-date to remaining needed count (threads 28-30)
- Catch JSONDecodeError in PATCH annotation → 400 (threads 31-32)

history.py:
- Catch JSONDecodeError in PATCH notes → 400 (same pattern as shots.py)

MarkdownEditor.tsx:
- Make handleSave async, await onSave, keep editor open on failure (thread 33)
- Update onSave prop type to support Promise<void>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hessius
Copy link
Owner Author

hessius commented Mar 11, 2026

All 12 Open Review Threads Addressed (828f6ae)

CI running. All changes verified locally: 750 backend tests + 277 frontend tests pass.

Code Fixes (4 files changed)

Thread File Fix
#25 shots.py Safe _safe_float() helper for timestamp parsing — no more ValueError on bad data
#26-27 shots.py by-profile endpoint now applies offset before grouping
#28-30 shots.py File enqueuing per-date limited to remaining = needed - len(all_shots)
#31-32 shots.py JSONDecodeError caught → returns 400, not 500
history.py Same JSON decode fix applied to PATCH notes endpoint
#33 MarkdownEditor.tsx handleSave is now async, awaits onSave, keeps editor open on failure

New Tests (9 tests added → 750 total)

Thread Tests
#34-35 TestHistoryNotesEndpoints — 5 tests (GET/PATCH success, 404, clear notes)
#36 TestMachineDetectEndpoint — 4 tests (found+verified, found+unverified, not found, exception)

Thread Resolution Summary (all 36 threads)

Status Count Details
Resolved (outdated/stale) 14 Code changed since review
Resolved (already addressed) 10 getServerUrl await, annotation lock, dual routes, etc.
Newly fixed 12 All implemented above — zero deferrals

Deferred items: None. Zero tech debt.

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.

Introduce MachineService abstraction layer for multi-backend support

3 participants