Skip to content

Gamify: editor engagement, feature discovery & account persistence (epic #796) - #820

Merged
fernandotonon merged 6 commits into
masterfrom
feat/gamification-editor-epic-796
Jul 6, 2026
Merged

Gamify: editor engagement, feature discovery & account persistence (epic #796)#820
fernandotonon merged 6 commits into
masterfrom
feat/gamification-editor-epic-796

Conversation

@fernandotonon

@fernandotonon fernandotonon commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

Editor half of the gamification epic #796 — implements all six slices (E-P1 #797 … E-P6 #802) against the shipped qtmesh-cloud API (qtmesh-cloud#79 / PR #86).

E-P1 — Cloud plumbing + offline event queue (#797)

  • GamificationEventQueue: persistent, cross-process-safe (QLockFile) JSON queue in <AppData>/gamification/queue.json. Client-generated UUID idempotency ids (the server dedup key), capacity 500 with logged FIFO eviction — replays never double-award, nothing is dropped silently.
  • QtMeshCloudClient gains postEditorEvents (POST /v1/events/editor), postOperationEvents (POST /v1/events/operations), fetchGamificationStats (GET /v1/me/stats), fetch/setGamificationPrefs, deleteGamificationData, profileUrl() — same blocking-call + QTMESH_API_BASE pattern as the rest of the client.
  • GamificationManager (singleton, QML-exposed): debounced flush + 90 s heartbeat + graceful-shutdown flush on worker threads, exponential backoff (max 30 min); flushBlocking() for one-shot CLI processes. Newly-earned achievements come back from the flush response. Stats parse into a typed Gamification::StatsSnapshot (handles the wire's mixed snake/camel casing) and cache to disk for offline rendering.

E-P2 — Feature-usage instrumentation (#798)

  • GamificationManager::noteFeature("<key>", surface) one-liners at the entry points of every mapped cluster (GUI controllers, CLIPipeline::run subcommand map, MCPServer::callTool tool map). Deduped per session; events carry only {id, feature, at, surface}.
  • The 25 cluster keys live in Gamification::featureCatalog() and match qtmesh-cloud's DISCOVERY_FEATURES exactly.

E-P3 — Operation outcomes (#799)

  • noteOperation("<op>", {numeric metrics}) with before/after metrics at: retopo (GUI+CLI), decimate/LOD (GUI+CLI), optimize (CLI), uv_unwrap (GUI), auto_rig (GUI template/async/marker + CLI), skin_weights (GUI+CLI), fix (CLI), texture_atlas (GUI), isometric_sprites (GUI), vat_bake (GUI), vertex_color_bake (GUI), morph (GUI), motion_inbetween (GUI), pbr_synth (shared AIAssistManager core).
  • Metrics are filtered to numeric values only (numericMetricsOnly) so asset content/paths can never leak. Ops attach ownerSlug/projectSlug when a cloud project is being browsed (CloudProjectsController sets/clears the context).

E-P4 — In-app status surface (#800)

  • CloudAccountMenuButton menu gains a status block: level + XP progress bar, streak, nearest-unlockable achievement with progress (computed client-side from milestoneCatalog() + counters), and "View My Achievements…" → https://qtmesh.dev/u/<slug>. Stats refresh opportunistically on menu open; last-known snapshot renders offline.
  • GamificationToast: ONE restrained, coalescing, click-to-dismiss unlock toast (no animation).

E-P5 — Discovery nudges (#801)

  • Welcome screen shows at most one dismissible "try this next" card driven by GamificationManager.suggestion — personalized to unused clusters when stats exist, generic rotation when logged out. Rotation cursor + per-card dismissals + a master nudge toggle persist in QSettings.

E-P6 — Privacy, consent & offline correctness (#802)

  • Default OFF. Nothing is queued before the one-time, non-blocking consent prompt (shown on the first would-be event while signed in) is answered, or "Sync my QtMesh progress" is enabled in Preferences.
  • Preferences → General gains a "Progress Sync" section: master + per-stream toggles, "What exactly is shared?" expandable example payload, public-profile toggle (PUTs /v1/me/gamification/prefs), and a two-click "Delete my gamification data" (server purge + local queue/cache clear).
  • Zero network when logged out or opted out (events queue locally and flush after the next sign-in); CLI honours --no-telemetry.

Tests

  • GamificationTypes_test: catalog/contract keys, wire-payload parsing, cache round-trip, nearest-unlockable ranking.
  • GamificationEventQueue_test: persistence, dedup, ack, FIFO eviction, cross-instance merge.
  • GamificationManager_test: consent gating, session dedup, numeric-metric filtering, stream opt-outs, logged-out no-network flush, full flush against a local HTTP mock (batch shape + achievement toast signal), delete-data, suggestion rotation.

Closes #797, closes #798, closes #799, closes #800, closes #801, closes #802. Part of #796.

🤖 Generated with Claude Code


Follow-up commits (review + testing feedback)

  • Bot-review fixes (c6527ce): --no-telemetry suspends all emission process-wide; consent prompt only consumed with a GUI listener; queue tombstones prevent stale-snapshot resurrection; stream opt-outs drop queued events; owner-stamped entries never post across accounts; delete-my-data serialized with flushes; no prefs traffic when opted out. Plus the unit-tests-linux link fix (gamification sources added to qtmesh_test_common).
  • Sonar reliability (7941977, 28eab02): worker-thread callbacks guard QCoreApplication::instance() against app teardown; setProfilePublic callback restructured for cpp:S2259.
  • Account-menu fixes from live testing (07ab6a2): gamification menu entries are physically inserted/removed (macOS QMenu paints hidden QWidgetActions — black strips + hover offset); "Enable Progress Sync…" action when signed-in-but-disabled; "Syncing progress…" placeholder; stats fetch fires immediately on enable. CloudAccountMenuButton tests made hermetic (no OS keychain probe).
  • New lighting discovery cluster (c427012): pairs with qtmesh-cloud#96 (deployed) — noteFeature("lighting") on light creation and HDR environment load; catalog now 25 clusters.
  • Queue consistency (28eab02): clear()/removeKind() re-hydrate from disk when the persisted removal fails.

Verified end-to-end against production: feature events from the running editor landed on the live account and the stats surface renders them.

Editor half of the gamification epic, built against the shipped
qtmesh-cloud API (qtmesh-cloud#79):

- E-P1 (#797): GamificationManager + persistent cross-process event
  queue with client idempotency ids; QtMeshCloudClient calls for
  POST /v1/events/editor, POST /v1/events/operations, GET /v1/me/stats,
  gamification prefs and data deletion; debounced/backoff flush loop,
  blocking flush for one-shot CLI processes, typed stats snapshot with
  an offline disk cache.
- E-P2 (#798): noteFeature() one-liners across the 24 feature clusters
  (GUI controllers, CLI subcommand map, MCP tool map), deduped per
  session; events carry only {id, feature, at, surface}.
- E-P3 (#799): noteOperation() with content-free numeric before/after
  metrics (retopo, decimate/LOD, optimize, uv_unwrap, auto_rig,
  skin_weights, fix, texture_atlas, isometric_sprites, vat_bake,
  vertex_color_bake, morph, motion_inbetween, pbr_synth); attaches the
  active cloud project context when one is open.
- E-P4 (#800): level/XP/streak/nearest-unlockable status block in the
  cloud account menu, one coalescing unlock toast, achievements deep
  link to qtmesh.dev/u/<slug>.
- E-P5 (#801): dismissible "try this next" welcome-screen card for
  unused clusters (personalized when stats exist, generic logged out).
- E-P6 (#802): default-off consent (one-time non-blocking prompt),
  Preferences "Progress Sync" section with per-stream toggles, example
  payload, public-profile toggle and delete-my-data; zero network when
  logged out or opted out; CLI honours --no-telemetry.

Closes #797, closes #798, closes #799, closes #800, closes #801,
closes #802. Part of #796.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a gamification and progress-sync subsystem: persistent event storage, a manager for consent/gating/flush/stats/suggestions, cloud API support, new UI surfaces, and feature/operation instrumentation across editor, CLI, and MCP entry points.

Changes

Gamification / Progress Sync

Layer / File(s) Summary
Gamification data contracts
src/GamificationTypes.h, src/GamificationTypes.cpp, src/GamificationTypes_test.cpp
Defines feature and milestone catalogs, stats/achievement parsing and serialization, derived unlockable progress, and unit tests.
Persistent event queue
src/GamificationEventQueue.h, src/GamificationEventQueue.cpp, src/GamificationEventQueue_test.cpp
Implements the file-locked JSON queue with append, acknowledge, removeKind, clear, reload, FIFO eviction, tombstones, and tests.
GamificationManager orchestration
src/GamificationManager.h, src/GamificationManager.cpp, src/AppSettingsKeys.h, src/GamificationManager_test.cpp
Adds consent and emission gating, queue flushing, stats caching, suggestion rotation, cloud-data deletion, profile controls, and settings keys, with tests.
Cloud API client
src/QtMeshCloudClient.h, src/QtMeshCloudClient.cpp
Adds APIs for posting events, fetching stats and prefs, updating prefs, deleting gamification data, and building profile URLs.
Build wiring
src/CMakeLists.txt, tests/CMakeLists.txt
Adds the new gamification sources and headers to the application and test build targets.
Gamification UI surfaces
src/GamificationToast.h, src/GamificationToast.cpp, qml/PreferencesDialog.qml, qml/WelcomeScreen.qml, src/CloudAccountMenuButton.h, src/CloudAccountMenuButton.cpp, src/mainwindow.cpp
Adds the achievement toast, progress-sync settings, suggestion card, cloud menu status block, and MainWindow wiring for consent and stats updates.
GUI/editor instrumentation
src/AIAssistManager.cpp, src/AnimationBlender.cpp, src/AnimationControlController.cpp, src/AutoRigController.cpp, src/BatchExporter.cpp, src/CloudProjectsController.cpp, src/ImageTo3D/MeshGenController.cpp, src/IsometricSpritesController.cpp, src/LLMManager.cpp, src/MaterialEditorQML.cpp, src/MaterialPresetLibrary.cpp, src/MeshDecimatorController.cpp, src/MeshLodController.cpp, src/MorphAnimationManager.cpp, src/PoseLibrary.cpp, src/QuadRetopoController.cpp, src/SDManager.cpp, src/SkinWeightsController.cpp, src/TexturePaintController.cpp, src/UVEditorController.cpp, src/UvUnwrapController.cpp, src/VATBakerController.cpp
Adds feature and operation notes around successful editor actions.
CLI and MCP instrumentation
src/CLIPipeline.cpp, src/MCPServer.cpp
Adds gamification notes in CLI command paths and MCP tool success handling.
Documentation
CLAUDE.md
Documents the gamification architecture, privacy rules, queueing, cloud contract, UI surfaces, discovery behavior, and tracked operations.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Controller
  participant GamificationManager
  participant GamificationEventQueue
  participant QtMeshCloudClient

  Controller->>GamificationManager: noteFeature/noteOperation(key, metrics)
  GamificationManager->>GamificationEventQueue: append(entry)
  GamificationManager->>GamificationManager: scheduleFlushSoon()
  GamificationManager->>QtMeshCloudClient: postEditorEvents/postOperationEvents(batch)
  QtMeshCloudClient-->>GamificationManager: accepted ids, newAchievements
  GamificationManager->>GamificationEventQueue: acknowledge(ids)
  GamificationManager-->>Controller: achievementsUnlocked / suggestionChanged
Loading
sequenceDiagram
  participant GamificationManager
  participant MainWindow
  participant User
  participant GamificationToast

  GamificationManager->>MainWindow: consentPromptRequested()
  MainWindow->>User: show consent dialog
  User->>MainWindow: accept / decline
  MainWindow->>GamificationManager: acceptConsent() / declineConsent()
  GamificationManager->>MainWindow: achievementsUnlocked(achievements)
  MainWindow->>GamificationToast: showAchievements(achievements)
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses the linked gamification goals across queueing, instrumentation, status UI, nudges, and privacy controls.
Out of Scope Changes check ✅ Passed The diff stays focused on the gamification epic, with docs, tests, UI, and cloud/client changes only.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title is concise and accurately summarizes the main gamification-focused editor changes.
Description check ✅ Passed The description is detailed and on-topic, covering summary, technical details, tests, and follow-up fixes; only the template headings differ.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gamification-editor-epic-796

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cddf728c06

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/CLIPipeline.cpp
}

cliWrite(report);
GamificationManager::noteOperation(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate CLI operation events on --no-telemetry

For opted-in users, running a successful operation subcommand with --no-telemetry still reaches this unconditional noteOperation, which appends the event to the persistent gamification queue before run() checks s_noTelemetry; because the final block only skips flushing for this process, the queued operation can be sent on a later run/sign-in. This makes --no-telemetry fail for operation history (fix/decimate/retopo/etc.), so the CLI operation hooks need the same guard or the manager needs a per-process suppression flag.

Useful? React with 👍 / 👎.

return false;
if (!signedIn())
return false;
settings.setValue(AppSettingsKeys::gamificationConsentPrompted(), true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Mark consent prompted only when a prompt can appear

If the first would-be gamification event for a signed-in user comes from the CLI, there is no MainWindow connected to consentPromptRequested, but this still persists Gamification/consentPrompted=true; subsequent GUI events then return at the prompted check and never show the consent dialog, leaving progress sync off until the user manually finds Preferences. Since this commit adds CLI calls into noteFeature/noteOperation, the manager should avoid consuming the one-time prompt in headless contexts or set this flag from the UI after creating the prompt.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/CLIPipeline.cpp (1)

6686-6699: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Gate decimate_lod stats on report.applied (src/CLIPipeline.cpp:6715-6720)
cmdDecimate records decimate_lod unconditionally, so a reduction <= 0.0 no-op still credits a decimation event with unchanged counts. Mirror cmdOptimize and only call noteOperation(...) when report.applied is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/CLIPipeline.cpp` around lines 6686 - 6699, The decimation stats are being
recorded even when no decimation actually happened, so update cmdDecimate in
CLIPipeline.cpp to gate the noteOperation call on report.applied, matching the
pattern used by cmdOptimize. Locate the decimateEntity call and ensure the
decimate_lod operation is only credited when MeshDecimator::decimateEntity
returns a report with applied true, so no-op reductions do not increment stats.
🧹 Nitpick comments (6)
src/GamificationToast.cpp (1)

18-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a Sentry breadcrumb when a toast is shown.

Achievement unlocks are a significant, user-facing milestone. Based on learnings, breadcrumbs should be added for user-facing actions and significant operations using SentryReporter::addBreadcrumb(...), and this codebase applies that pattern extensively elsewhere (e.g. CloudAccountMenuButton.cpp's "View My Achievements…" handler). Consider adding one here too, e.g. in appendAchievements() when the toast is (re)shown.

Also applies to: 47-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GamificationToast.cpp` around lines 18 - 25, Add a Sentry breadcrumb for
the achievement toast display path so user-facing unlock events are captured
consistently. Update the toast flow in GamificationToast, ideally in
appendAchievements() (or immediately before it is called from
showAchievements()), to call SentryReporter::addBreadcrumb(...) with a message
describing that achievements were shown/re-shown. Keep the existing
parentWindow/empty-check behavior intact and use the same breadcrumb pattern
used elsewhere in the UI, such as the CloudAccountMenuButton achievement
handler, so the toast display is observable in Sentry.

Source: Learnings

src/mainwindow.cpp (2)

2824-2859: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider Sentry breadcrumbs for achievement toast + consent decisions.

Based on learnings, breadcrumbs should be added for user-facing actions and significant operations. Nearly every other user action in this file (menu clicks, dialog outcomes, etc.) is wrapped with SentryReporter::addBreadcrumb(...), but showing the achievement toast and the accept/decline consent outcome are not. Given consent state materially changes network behavior, a breadcrumb here would help diagnose sync-related support issues.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mainwindow.cpp` around lines 2824 - 2859, Add Sentry breadcrumbs around
the gamification UI actions in the GamificationManager signal handlers inside
the main window setup: when `GamificationToast::showAchievements(...)` is
triggered, record a breadcrumb before showing the toast, and in the
`QMessageBox::finished` lambda for the consent prompt, add breadcrumbs for both
the accept and decline paths before calling `acceptConsent()` or
`declineConsent()`. Use `SentryReporter::addBreadcrumb(...)` consistently with
the existing breadcrumb patterns in `src/mainwindow.cpp` so the achievement
toast and consent decision are visible in diagnostics.

Source: Learnings


802-809: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Register GamificationManager once src/mainwindow.cpp:802-809WelcomeScreen.qml already imports PropertiesPanel 1.0, so the extra WelcomeScreen alias is redundant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mainwindow.cpp` around lines 802 - 809, `GamificationManager` is being
registered twice with different module names, but `WelcomeScreen` is redundant
because `WelcomeScreen.qml` already imports `PropertiesPanel 1.0`. Remove the
extra `qmlRegisterSingletonType` for `WelcomeScreen` in `mainwindow.cpp` and
keep the single `PropertiesPanel` registration, using
`GamificationManager::qmlInstance` as the shared singleton source.
src/GamificationToast.h (1)

14-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

New UI surface built with Qt Widgets instead of QML.

This is an entirely new UI component (toast) implemented as a QWidget subclass. Based on learnings, new UI should be built with QML (Qt Quick), not Qt Widgets — and the rest of this PR's gamification UI (preferences panel, welcome screen nudge) correctly follows that pattern via a QML-exposed GamificationManager singleton. Consider hosting this toast as a small QML overlay (similar to the existing WelcomeScreen QQuickWidget overlay pattern in mainwindow.cpp) instead of a standalone QWidget.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GamificationToast.h` around lines 14 - 36, The new GamificationToast UI
is implemented as a standalone QWidget subclass, but it should follow the Qt
Quick/QML approach used elsewhere in this PR. Refactor GamificationToast
(including showAchievements, appendAchievements, reposition, and updateText)
into a QML-backed overlay instead of a QWidget, and host it through the existing
QQuickWidget-style overlay pattern used for the WelcomeScreen in mainwindow.cpp.
Keep the gamification data flow through GamificationManager, but move the toast
presentation into QML so the new surface matches the rest of the UI.

Source: Learnings

src/AutoRigController.cpp (1)

310-315: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated noteOperation("auto_rig", ...) block into a helper.

The same 5-line gamification block is duplicated verbatim across autoRigSelected, emitRigResult, and commitMarkerRig. Consolidating reduces the risk of the three call sites diverging if the metrics or gating condition change later.

♻️ Proposed refactor
+void AutoRigController::noteAutoRigOutcome(const AutoRig::Report& report, bool skinned)
+{
+    if (!report.applied)
+        return;
+    GamificationManager::noteOperation(
+        QStringLiteral("auto_rig"),
+        {{QStringLiteral("bones_created"), report.boneCount},
+         {QStringLiteral("meshes_skinned"), skinned ? 1 : 0}});
+}

Then at each call site:

-    if (report.applied)
-        GamificationManager::noteOperation(
-            QStringLiteral("auto_rig"),
-            {{QStringLiteral("bones_created"), report.boneCount},
-             {QStringLiteral("meshes_skinned"), skinned ? 1 : 0}});
+    noteAutoRigOutcome(report, skinned);

Also applies to: 348-353, 638-643

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/AutoRigController.cpp` around lines 310 - 315, Extract the duplicated
GamificationManager::noteOperation("auto_rig", ...) block into a single helper
in AutoRigController and reuse it from autoRigSelected, emitRigResult, and
commitMarkerRig. Keep the existing gating on report.applied and preserve the
bones_created and meshes_skinned metrics exactly as-is, then replace each
repeated inline block with the new helper call so the three paths stay in sync.
src/GamificationEventQueue.cpp (1)

117-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a Sentry breadcrumb for capacity eviction and data deletion.

Both FIFO eviction (silent data loss beyond a warning log) and clear() (user-initiated data deletion) are significant, user-impacting operations. Based on learnings, add SentryReporter::addBreadcrumb(...) for user-facing actions and significant operations, with the established categories, so these are visible in crash/session diagnostics alongside the existing qWarning() logging.

Also applies to: 185-192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GamificationEventQueue.cpp` around lines 117 - 125, Add Sentry
breadcrumbs for the user-impacting data removal paths in GamificationEventQueue
so they appear in crash/session diagnostics alongside the existing qWarning
logs. Update GamificationEventQueue::enforceCapacity to call
SentryReporter::addBreadcrumb(...) when FIFO eviction happens, and update
clear() to add a breadcrumb for user-initiated data deletion using the
established breadcrumb categories already used elsewhere. Keep the existing
warning logging, but ensure both eviction and clear operations are recorded with
consistent Sentry context.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@qml/PreferencesDialog.qml`:
- Around line 326-332: `PreferencesDialog.qml` currently calls
`GamificationManager.refreshCloudPrefs()` unconditionally from
`Component.onCompleted`, which still triggers a cloud fetch for signed-in users
who have opted out of gamification. Update the `Row`/`Component.onCompleted`
logic so the refresh only runs when both `GamificationManager.signedIn` and the
privacy/opt-out state allow gamification network traffic, using the existing
`GamificationManager` state checks to gate the call.

In `@src/GamificationEventQueue.cpp`:
- Around line 100-115: mergeFromDisk currently unions the in-memory snapshot
with disk state in GamificationEventQueue, which can resurrect events that were
already removed by acknowledge() or clear() in another process. Update
GamificationEventQueue::mergeFromDisk (and the related append/acknowledge/clear
flow) to track locally removed ids as tombstones or an equivalent exclusion set,
and filter those ids out when rebuilding m_entries so stale writers cannot
reintroduce processed events.
- Around line 185-192: `GamificationEventQueue::clear()` currently clears
`m_entries` even when the persisted wipe in `withFileLock()`/`saveLocked()`
fails, so the in-memory state can diverge from disk. Update
`GamificationEventQueue::clear` to only clear `m_entries` after the file-backed
clear and `saveLocked()` complete successfully, and propagate or handle any
failure from the lock/save path before mutating in-memory state.

In `@src/GamificationEventQueue.h`:
- Around line 49-50: The GamificationEventQueue::clear() API cannot surface
lock/persistence failures to its callers, so update the method contract to
return a success/failure signal instead of void and propagate the result through
the corresponding implementation in the .cpp. Adjust the clear() declaration and
definition, and make GamificationManager’s delete-my-data flow check the
returned status so it can detect and retry a failed wipe when the lock cannot be
acquired.

In `@src/GamificationManager.cpp`:
- Around line 257-267: Queued gamification batches are still being sent after a
stream is disabled, so update the queue handling in GamificationManager to
respect the current usage/ops toggles. In flushNow(), and any related batch
selection logic, filter out or suppress queued feature/operation entries when
setUsageEnabled(false) or setOpsEnabled(false) has been applied, and make sure
the existing stream-specific paths in GamificationManager::setUsageEnabled and
GamificationManager::setOpsEnabled cannot allow already-queued items through.
- Around line 720-756: The delete flow in GamificationManager::deleteCloudData
does not serialize against pending flushes, so a queued or in-flight upload can
repopulate server data after deletion; update this path to pause/defer flushing
until any active flush completes or is invalidated. Also clear the local queued
events and session-related state immediately as part of the local half of the
request, rather than waiting for the server DELETE success path. Keep the
success callback focused on removing remaining local cache/snapshot state and
emitting the completion signal, using the existing deleteCloudData, m_queue,
m_sessionNotedFeatures, and statsChanged/deleteCloudDataFinished logic.
- Around line 612-623: The account-change handling in handleSessionChanged()
keeps queued work across sign-out, which lets one user’s queued metadata be
flushed under the next signed-in account. Update the sign-out path to clear or
partition all account-scoped state, including m_sessionNotedFeatures and any
active project/context used by the queue, or else persist and flush queued items
only for the same account. Keep the existing snapshot reset and stats file
removal, but make sure queued bodies cannot survive an account boundary unless
they are tied to the original user.

---

Outside diff comments:
In `@src/CLIPipeline.cpp`:
- Around line 6686-6699: The decimation stats are being recorded even when no
decimation actually happened, so update cmdDecimate in CLIPipeline.cpp to gate
the noteOperation call on report.applied, matching the pattern used by
cmdOptimize. Locate the decimateEntity call and ensure the decimate_lod
operation is only credited when MeshDecimator::decimateEntity returns a report
with applied true, so no-op reductions do not increment stats.

---

Nitpick comments:
In `@src/AutoRigController.cpp`:
- Around line 310-315: Extract the duplicated
GamificationManager::noteOperation("auto_rig", ...) block into a single helper
in AutoRigController and reuse it from autoRigSelected, emitRigResult, and
commitMarkerRig. Keep the existing gating on report.applied and preserve the
bones_created and meshes_skinned metrics exactly as-is, then replace each
repeated inline block with the new helper call so the three paths stay in sync.

In `@src/GamificationEventQueue.cpp`:
- Around line 117-125: Add Sentry breadcrumbs for the user-impacting data
removal paths in GamificationEventQueue so they appear in crash/session
diagnostics alongside the existing qWarning logs. Update
GamificationEventQueue::enforceCapacity to call
SentryReporter::addBreadcrumb(...) when FIFO eviction happens, and update
clear() to add a breadcrumb for user-initiated data deletion using the
established breadcrumb categories already used elsewhere. Keep the existing
warning logging, but ensure both eviction and clear operations are recorded with
consistent Sentry context.

In `@src/GamificationToast.cpp`:
- Around line 18-25: Add a Sentry breadcrumb for the achievement toast display
path so user-facing unlock events are captured consistently. Update the toast
flow in GamificationToast, ideally in appendAchievements() (or immediately
before it is called from showAchievements()), to call
SentryReporter::addBreadcrumb(...) with a message describing that achievements
were shown/re-shown. Keep the existing parentWindow/empty-check behavior intact
and use the same breadcrumb pattern used elsewhere in the UI, such as the
CloudAccountMenuButton achievement handler, so the toast display is observable
in Sentry.

In `@src/GamificationToast.h`:
- Around line 14-36: The new GamificationToast UI is implemented as a standalone
QWidget subclass, but it should follow the Qt Quick/QML approach used elsewhere
in this PR. Refactor GamificationToast (including showAchievements,
appendAchievements, reposition, and updateText) into a QML-backed overlay
instead of a QWidget, and host it through the existing QQuickWidget-style
overlay pattern used for the WelcomeScreen in mainwindow.cpp. Keep the
gamification data flow through GamificationManager, but move the toast
presentation into QML so the new surface matches the rest of the UI.

In `@src/mainwindow.cpp`:
- Around line 2824-2859: Add Sentry breadcrumbs around the gamification UI
actions in the GamificationManager signal handlers inside the main window setup:
when `GamificationToast::showAchievements(...)` is triggered, record a
breadcrumb before showing the toast, and in the `QMessageBox::finished` lambda
for the consent prompt, add breadcrumbs for both the accept and decline paths
before calling `acceptConsent()` or `declineConsent()`. Use
`SentryReporter::addBreadcrumb(...)` consistently with the existing breadcrumb
patterns in `src/mainwindow.cpp` so the achievement toast and consent decision
are visible in diagnostics.
- Around line 802-809: `GamificationManager` is being registered twice with
different module names, but `WelcomeScreen` is redundant because
`WelcomeScreen.qml` already imports `PropertiesPanel 1.0`. Remove the extra
`qmlRegisterSingletonType` for `WelcomeScreen` in `mainwindow.cpp` and keep the
single `PropertiesPanel` registration, using `GamificationManager::qmlInstance`
as the shared singleton source.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97ff7967-00e6-4fda-84b4-2fc98f55bd8a

📥 Commits

Reviewing files that changed from the base of the PR and between db75d79 and cddf728.

📒 Files selected for processing (45)
  • CLAUDE.md
  • qml/PreferencesDialog.qml
  • qml/WelcomeScreen.qml
  • src/AIAssistManager.cpp
  • src/AnimationBlender.cpp
  • src/AnimationControlController.cpp
  • src/AppSettingsKeys.h
  • src/AutoRigController.cpp
  • src/BatchExporter.cpp
  • src/CLIPipeline.cpp
  • src/CMakeLists.txt
  • src/CloudAccountMenuButton.cpp
  • src/CloudAccountMenuButton.h
  • src/CloudProjectsController.cpp
  • src/GamificationEventQueue.cpp
  • src/GamificationEventQueue.h
  • src/GamificationEventQueue_test.cpp
  • src/GamificationManager.cpp
  • src/GamificationManager.h
  • src/GamificationManager_test.cpp
  • src/GamificationToast.cpp
  • src/GamificationToast.h
  • src/GamificationTypes.cpp
  • src/GamificationTypes.h
  • src/GamificationTypes_test.cpp
  • src/ImageTo3D/MeshGenController.cpp
  • src/IsometricSpritesController.cpp
  • src/LLMManager.cpp
  • src/MCPServer.cpp
  • src/MaterialEditorQML.cpp
  • src/MaterialPresetLibrary.cpp
  • src/MeshDecimatorController.cpp
  • src/MeshLodController.cpp
  • src/MorphAnimationManager.cpp
  • src/PoseLibrary.cpp
  • src/QtMeshCloudClient.cpp
  • src/QtMeshCloudClient.h
  • src/QuadRetopoController.cpp
  • src/SDManager.cpp
  • src/SkinWeightsController.cpp
  • src/TexturePaintController.cpp
  • src/UVEditorController.cpp
  • src/UvUnwrapController.cpp
  • src/VATBakerController.cpp
  • src/mainwindow.cpp

Comment thread qml/PreferencesDialog.qml Outdated
Comment thread src/GamificationEventQueue.cpp
Comment thread src/GamificationEventQueue.cpp Outdated
Comment on lines +185 to +192
void GamificationEventQueue::clear()
{
withFileLock([this]() {
m_entries.clear();
saveLocked();
});
m_entries.clear();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first
ast-grep outline src/GamificationEventQueue.cpp --view expanded || true

# Read targeted sections around the relevant functions
nl -ba src/GamificationEventQueue.cpp | sed -n '1,260p'

# Find call sites/usages of clear()
rg -n "\.clear\(\)|GamificationEventQueue::clear|clear\(" src --glob '!**/build/**' || true

Repository: fernandotonon/QtMeshEditor

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the file in smaller chunks if needed
nl -ba src/GamificationEventQueue.cpp | sed -n '1,220p'

Repository: fernandotonon/QtMeshEditor

Length of output: 204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and print its contents in safe slices if present.
git ls-files | rg -n 'GamificationEventQueue\.cpp$|GamificationEventQueue\.h$' || true

file=$(git ls-files | rg 'GamificationEventQueue\.cpp$' | head -n 1 || true)
if [ -n "${file:-}" ]; then
  echo "== $file =="
  sed -n '1,260p' "$file"
fi

# Also show nearby references to clear()/withFileLock() if the file exists.
if [ -n "${file:-}" ]; then
  rg -n 'withFileLock|clear\(|acknowledge\(|mergeFromDisk|saveLocked|append\(' "$file" || true
fi

Repository: fernandotonon/QtMeshEditor

Length of output: 7116


clear() should only drop in-memory state after the persisted wipe succeeds.
withFileLock() can fail, and saveLocked() is ignored; in both cases the final m_entries.clear() still runs, so a failed delete leaves the file intact and the next append()/mergeFromDisk() can bring the entries back. src/GamificationEventQueue.cpp:185-192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GamificationEventQueue.cpp` around lines 185 - 192,
`GamificationEventQueue::clear()` currently clears `m_entries` even when the
persisted wipe in `withFileLock()`/`saveLocked()` fails, so the in-memory state
can diverge from disk. Update `GamificationEventQueue::clear` to only clear
`m_entries` after the file-backed clear and `saveLocked()` complete
successfully, and propagate or handle any failure from the lock/save path before
mutating in-memory state.

Comment thread src/GamificationEventQueue.h Outdated
Comment thread src/GamificationManager.cpp
Comment thread src/GamificationManager.cpp
Comment thread src/GamificationManager.cpp
@fernandotonon

Copy link
Copy Markdown
Owner Author

Addressed the review feedback (Codex + CodeRabbit) in the follow-up commit:

  • --no-telemetry now gates ALL gamification emission (P1): GamificationManager::setEmissionSuspended(true) is set process-wide before subcommand dispatch, so operation notes inside cmdFix/cmdRetopo/cmdDecimate/… can no longer queue events that a later run would flush.
  • Consent prompt is no longer consumed headlessly (P2): maybeRequestConsent() only sets Gamification/consentPrompted and emits when a receiver is actually connected to consentPromptRequested (i.e. a GUI session).
  • clear() is now fallible + tombstoned: returns false when the persisted wipe fails (in-memory state kept for retry); ids removed by ack/clear/removeKind are tombstoned so a concurrent writer's stale snapshot can never resurrect them (StaleSnapshotCannotResurrectRemovedIds test).
  • Stream opt-outs now apply retroactively: disabling feature-usage/operations drops that stream's queued events (removeKind) and flush gates each batch on the live toggle.
  • Account switches can't cross-post: queue entries are stamped with the owning user slug at enqueue; flush only sends entries owned by the current account (or unclaimed logged-out ones) and drops foreign entries (EventsFromAnotherAccountAreDroppedNotSent test).
  • Delete-my-data is serialized with flushing: local queue/cache are cleared immediately, new flushes are blocked while the delete is pending, and a delete requested during an in-flight flush is deferred until that flush settles.
  • No prefs fetch when opted out: refreshCloudPrefs()/setProfilePublic() early-return unless sync is enabled, and the Preferences row only shows (and fetches) when signed in and sync is on.
  • cmdDecimate operation note is now gated on report.applied.
  • Also fixed the unit-tests-linux link failure: the gamification sources/headers are now in tests/CMakeLists.txt's qtmesh_test_common.

🤖 Generated with Claude Code

Review fixes (Codex P1/P2 + CodeRabbit):
- --no-telemetry now suspends ALL gamification emission process-wide
  (GamificationManager::setEmissionSuspended), so CLI operation notes
  inside subcommands can no longer queue events that a later run would
  flush.
- The one-time consent prompt is only consumed when a GUI listener is
  connected to consentPromptRequested — headless CLI/MCP runs no longer
  burn the user's only chance to see the dialog.
- Queue removals (acknowledge/clear/removeKind) are tombstoned so a
  concurrent writer's stale in-memory snapshot can never resurrect
  removed events; clear() is now fallible and keeps state for retry
  when the persisted wipe fails.
- Disabling a stream (feature usage / operations) also drops that
  stream's already-queued events, and flush gates each batch on the
  live toggle.
- Queue entries are stamped with the owning user slug; flush drops
  (never posts) entries recorded for a different account.
- Delete-my-data clears local queue/cache immediately, blocks new
  flushes while pending, and defers the server DELETE until an
  in-flight flush settles.
- refreshCloudPrefs()/setProfilePublic() early-return unless sync is
  enabled; the Preferences public-profile row only shows (and fetches)
  when signed in and sync is on.
- CLI decimate operation note gated on report.applied.

Also adds the gamification sources/headers to tests/CMakeLists.txt's
qtmesh_test_common, fixing the unit-tests-linux link failure
(undefined GamificationManager symbols in MaterialEditorQML_test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/GamificationEventQueue.cpp (1)

206-224: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

clear() mutates m_entries before knowing whether the save succeeded — the "kept in-memory for retry" contract is not actually honored.

m_entries.clear() (line 214) runs unconditionally inside the lock callback, and only afterward is saved = saveLocked() evaluated. If saveLocked() fails, the function returns false, but the in-memory queue is already empty — the events are gone from both disk (unwritten) and memory, with no tombstone recorded (tombstone only runs on success at line 222). This contradicts the header's documented contract that "Returns false when the persisted wipe failed (lock or write) — in-memory entries are kept in that case so the caller can retry." This same defect was raised on a prior version of this function; the added bool return signals failure but doesn't actually preserve state.

🐛 Proposed fix
 bool GamificationEventQueue::clear()
 {
     QSet<QString> ids;
     bool saved = false;
-    const bool locked = withFileLock([this, &ids, &saved]() {
+    QList<Entry> snapshot;
+    const bool locked = withFileLock([this, &ids, &saved, &snapshot]() {
         mergeFromDisk();
+        snapshot = m_entries;
         for (const Entry& e : std::as_const(m_entries))
             ids.insert(e.id);
         m_entries.clear();
         saved = saveLocked();
+        if (!saved)
+            m_entries = snapshot;
     });
     if (!locked || !saved) {
         // Persisted wipe failed: keep state so the caller can retry, and
         // report the failure instead of pretending the data is gone.
         return false;
     }
     tombstone(ids);
     return true;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/GamificationEventQueue.cpp` around lines 206 - 224,
`GamificationEventQueue::clear()` clears `m_entries` before confirming that
`saveLocked()` succeeded, so the in-memory retry contract is broken. Update the
`clear()` flow to preserve `m_entries` unless the persisted wipe is successfully
saved, using the existing `withFileLock`, `saveLocked`, and `tombstone` logic so
that state is only removed from memory after a successful write.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/GamificationEventQueue.cpp`:
- Around line 226-246: The removeKind() flow in GamificationEventQueue mutates
m_entries before confirming saveLocked() succeeded, so a failed save leaves
in-memory state inconsistent. Update removeKind() to follow the same safe
pattern as a confirmed write: collect matching ids inside the withFileLock
callback, only apply removals to m_entries after saveLocked() returns true, and
keep tombstone(ids) strictly on the success path. Use removeKind(),
saveLocked(), tombstone(), and withFileLock() to locate and refactor the logic.

In `@tests/CMakeLists.txt`:
- Around line 83-86: The gamification test sources are listed in the shared test
library but never attached to a runnable test target, so the suites will not be
built or discovered by CTest. Update tests/CMakeLists.txt by registering the
gamification test executables for GamificationTypes_test,
GamificationEventQueue_test, and GamificationManager_test, using the existing
qtmesh_test_common setup and the corresponding test source files so they are
compiled and added as CTest entries.

---

Duplicate comments:
In `@src/GamificationEventQueue.cpp`:
- Around line 206-224: `GamificationEventQueue::clear()` clears `m_entries`
before confirming that `saveLocked()` succeeded, so the in-memory retry contract
is broken. Update the `clear()` flow to preserve `m_entries` unless the
persisted wipe is successfully saved, using the existing `withFileLock`,
`saveLocked`, and `tombstone` logic so that state is only removed from memory
after a successful write.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8b876f8-f1a5-47a7-9743-6164a1bc24fd

📥 Commits

Reviewing files that changed from the base of the PR and between cddf728 and c6527ce.

📒 Files selected for processing (9)
  • qml/PreferencesDialog.qml
  • src/CLIPipeline.cpp
  • src/GamificationEventQueue.cpp
  • src/GamificationEventQueue.h
  • src/GamificationEventQueue_test.cpp
  • src/GamificationManager.cpp
  • src/GamificationManager.h
  • src/GamificationManager_test.cpp
  • tests/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/GamificationEventQueue.h
  • src/GamificationManager.h
  • qml/PreferencesDialog.qml
  • src/GamificationManager.cpp

Comment thread src/GamificationEventQueue.cpp
Comment thread tests/CMakeLists.txt
fernandotonon and others added 4 commits July 6, 2026 10:37
QCoreApplication::instance() can be null when a flush/stats/prefs worker
finishes during application shutdown — check it before invokeMethod in
all five gamification worker lambdas (Sonar new_reliability_rating).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Physically insert/remove the gamification menu entries instead of
  setVisible(false): macOS QMenu keeps painting hidden QWidgetActions
  (black progress-bar strips) and mis-tracks hover geometry, which
  offset the highlight on the first menu items.
- Show the section in every signed-in state: "Enable Progress Sync…"
  action when sync is off (one-click consent + stats fetch), a
  "Syncing progress…" placeholder while the first stats fetch is in
  flight, and the level/XP/streak block once stats land.
- Fetch stats immediately when sync is enabled (setSyncEnabled /
  acceptConsent) instead of waiting for the next menu open.
- Make CloudAccountMenuButton tests hermetic: pre-set
  Cloud/legacyMigrationDone so refresh() never probes the OS keychain
  from tests (on a dev machine with a legacy session it imported the
  real token into the test scope and broke signed-out expectations).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pairs with qtmesh-cloud#96 (DISCOVERY_FEATURES += lighting →
`first_lighting` "Illuminator" achievement, Cartographer threshold 25):

- featureCatalog() gains the `lighting` entry (nudge card copy:
  "Scene Lighting").
- noteFeature("lighting") on LightsController::addLight (covers all
  toolbar/viewport add-light paths) and on a successful
  HdrEnvironmentController::loadEnvironment (Inspector HDR picker,
  choice list and Browse all funnel through it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- clear()/removeKind(): re-hydrate the in-memory queue from disk when
  saveLocked() fails, so a failed wipe never leaves memory claiming the
  entries are gone while the file still has them (CodeRabbit).
- setProfilePublic callback: hoist QPointer::data() into a checked raw
  pointer and drop the self-assigning ternary — Sonar's analyzer flagged
  the double self-> read as a possible null dereference (cpp:S2259).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

@fernandotonon
fernandotonon merged commit ac84edf into master Jul 6, 2026
37 of 38 checks passed
@fernandotonon
fernandotonon deleted the feat/gamification-editor-epic-796 branch July 6, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant