Skip to content

fix: achievements XP row collision + queued completions never counted toward review floor - #403

Merged
thomasluizon merged 6 commits into
mainfrom
fix/achievements-xp-row
Jul 6, 2026
Merged

fix: achievements XP row collision + queued completions never counted toward review floor#403
thomasluizon merged 6 commits into
mainfrom
fix/achievements-xp-row

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Two fixes surfaced during #382 QA:

1. Achievements header stats collision (both platforms)

At high levels/locales the two stat strings ("48.409 XP no total" + "14 / 39 conquistas desbloqueadas") overflowed the row and rendered jammed together. Fixed by shortening the copy in both locales ({total} XP / {count}/{total} conquistas — the full current/next XP line already sits directly above) and hardening the row (gap + flexShrink on mobile, gap on web) so no value length can collide again.

2. Queued completions never counted toward the review-moment floor (mobile)

useLogHabit.onSuccess early-returns for offline-queued results before trackCompletion, so any completion that went through the offline queue silently never counted toward review-moment eligibility (and the queue flush bypasses the mutation callbacks entirely). Moved the floor tracking to onMutate: an engagement counter tolerates a rare overcount on a failed request, but must not systematically miss queued completions. Regression test added.

Found live: a momentary network hiccup queued a completion, today's active-day never counted, and the post-celebration review sheet (#382) silently skipped an eligible level-up moment.

Validation: mobile 930 ✓ / web 2105 ✓ / shared 1384 ✓, lint 0 errors, type-check clean.

🤖 Generated with Claude Code

3. Celebration overlays never mounted on the Today route (mobile)

showSharedCelebrations = pathname !== '/' (added 2026-04-05 alongside the proper acknowledgedLevel re-trigger fix) excluded StreakCelebration, AllDoneCelebration, WelcomeBackToast, AchievementToast, and LevelUpOverlay from the Today tab — the screen where completions actually happen. Celebrations queued there could not play until the user navigated away, and the stalled queue also suppressed every post-celebration prompt (milestone share, referral, and the new #382 review moment) while on Today. Web mounts celebrations on all routes; mobile now matches. The infinite re-trigger bug that gate papered over was already properly fixed by acknowledgedLevel in the same April commit.

4. Agent confirmation cards showed English + raw policy codes (both platforms)

The pending-operation card rendered the server's English capability DisplayName/summary verbatim in pt-BR sessions, leaked raw policy reason codes (confirmation_required) as error text, and echoed the English summary as the success line. Confirmation-gated capability names (12) now localize client-side via a shared key mapping (getAgentCapabilityLabelKey, hyphen-folded ids, English fallback for unmapped capabilities), the summary is composed locally, known policy reasons map to friendly copy (getAgentPolicyReasonKey, generic error fallback — codes never render), and the success line is localized. en + pt-BR, tests on both platforms.

…completions toward review floor

- shorten profileCard.totalXp/earned strings (both locales) and harden the
  stats row (gap + flexShrink) so long values never collide
- track review-floor completions in onMutate instead of onSuccess: queued
  offline completions early-return before onSuccess and were never counted,
  silently blocking review-moment eligibility

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

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jul 6, 2026 4:44pm

Request Review

The pathname !== '/' gate (added Apr 5 alongside the proper acknowledgedLevel
re-trigger fix) excluded StreakCelebration, AllDoneCelebration, WelcomeBackToast,
AchievementToast, and LevelUpOverlay from the Today tab — the screen where
completions happen. Celebrations queued there could not play until the user
navigated away, and the stalled queue also suppressed every post-celebration
prompt. Web mounts celebrations on all routes; mobile now matches.

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

@claude claude 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.

/pr-review — PR #403

Scope: 7 files, +30/-9. Two independent fixes: (1) achievements header stat-row collision (web + mobile + both locales), (2) offline-queued habit completions never counting toward the post-celebration review-moment floor (mobile).

Findings

None at Critical/High. No concretely-actionable Medium findings survived review either.

Verification performed

  • Cross-platform parity: Both platforms received the collision fix — mobile (achievements.tsx) adds gap: 12 on the row + flexShrink: 1 on the shrinking child (needed because RN defaults flexShrink: 0, unlike CSS flexbox); web (achievement-xp-card.tsx) adds the equivalent gap: 12. Change #2 (use-habits.ts onMutate timing fix) is mobile-only by design — it fixes behavior inside review-reminder-store, which is a pre-existing mobile-only engagement feature (added in #382/#398) with no web equivalent to mirror.
  • i18n: gamification.profileCard.totalXp and .earned shortened in both en.json and pt-BR.json in the same diff. All callsites (web card, mobile screen, mobile i18n test) pass matching interpolation variables. No MISSING_EN/MISSING_PT/orphaned-key issues.
  • Logic correctness of the onMutate move: trackCompletion now fires in onMutate instead of onSuccess, so it runs before the offline queue can swallow the callback and before cancellation/response resolution. onError intentionally does not roll it back — the PR description explicitly frames this as an accepted tradeoff (rare overcount on a failed request vs. systematic miss on queued completions), and the store's role is an engagement-eligibility floor, not an exact counter — reasonable. A regression test (use-habits.test.ts) exercises onMutate directly and asserts completionCount/activeDays update immediately.
  • Backward compatibility / API contract: No packages/shared type or endpoint changes, no orbit-api touch — not applicable to this diff.
  • Code standards: No any, no console.log, no narration comments, functions stay well under size caps.

Recommendation: Approve

Small, well-scoped, correctly paired across platforms, with a regression test covering the behavioral fix.

@claude claude 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.

/pr-review — PR #403

Scope of this review: incremental diff since my prior approval at 6ee90b39 (already APPROVED). One new commit was added since then: a41d4088 "fix(mobile): mount celebration overlays on the Today route", touching only apps/mobile/app/_layout.tsx (+6/-14).

What changed

Removes showSharedCelebrations = pathname !== '/' and the five conditional wrappers it gated (StreakCelebration, AllDoneCelebration, WelcomeBackToast, AchievementToast, LevelUpOverlay), and threads the now-unused showSharedCelebrations prop out of GlobalOverlays. Previously these overlays never mounted on the Today route (pathname === '/') — the screen where completions actually happen — so queued celebrations and the post-celebration prompts (milestone share, referral, #382 review moment) silently stalled until the user navigated away.

Verification performed

  • No dead code left behind. Grepped the full file post-change — zero remaining references to showSharedCelebrations; the prop and its type were removed cleanly from both the caller and GlobalOverlays's destructured/typed params. pathname itself is still used elsewhere in the file (tab detection, back-fallback routing), so it wasn't orphaned.
  • Re-trigger risk is a non-issue. GlobalOverlays is mounted once alongside the root stack, not inside the route stack, so it never unmounts/remounts on navigation — the pathname gate only toggled which children rendered, it never protected against remount-driven re-fires. The "infinite re-trigger" bug the commit message references was independently fixed via acknowledgedLevel state in apps/mobile/hooks/use-gamification.ts (mirrored in apps/web/hooks/use-gamification.ts and packages/shared/src/utils/gamification-selectors.ts), which gates on level value, not route — confirmed unaffected by this diff.
  • True cross-platform parity. apps/web/app/(app)/layout.tsx (lines 282–288) renders the same five overlays unconditionally with no pathname gate. Mobile's _layout.tsx now matches exactly — this diff closes a parity gap (mandatory per the repo's cross-platform rule) rather than opening one.
  • No new surface for other rubric dimensions. No comments, no any/type changes, no i18n, no packages/shared/DTO changes, no security-relevant code in this commit.
  • Cross-repo dimensions (contract-aligner, orbit-api side of the backward-compat guard): not applicable — this commit touches no contract/shared-type surface, and orbit-api isn't checked out in this job.

Findings

None at Critical/High. No concretely-actionable Medium findings either. (Earlier commits in this PR were already reviewed and approved at 6ee90b39 with no findings — not re-flagged here.)

Recommendation: Approve

Small, correctly scoped fix that closes a genuine cross-platform parity gap, verified to have no dead code and no interaction with the separately-fixed re-trigger guard.

thomasluizon and others added 2 commits July 6, 2026 12:52
…ing policy codes

The pending-operation card rendered the server's English capability
DisplayName/summary verbatim and raw policy reason codes
(confirmation_required) as error text. Confirmation-gated capability
names now localize client-side via shared key mapping with English
fallback, the summary line is composed locally, known policy reasons
map to friendly copy, and the success line is localized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
useBulkLogHabits never called trackCompletion, so habits completed via
the Today screen's bulk action counted as zero toward review-moment
eligibility. Each bulk item now counts at mutate time, matching the
single-log path.

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

@claude claude 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.

Code Review: PR #403 — fix: achievements XP row collision + queued completions never counted toward review floor

Recommendation: APPROVE

Summary

Four bundled fixes surfaced during #382 QA: (1) an achievements-header XP/count row text collision at high locale/level values, (2) offline-queued habit completions never counting toward the post-celebration review-moment engagement floor, (3) celebration overlays never mounting on the Today route, and (4) the agent (Astra) confirmation card leaking raw English capability names and raw policy-reason codes in pt-BR sessions. All four land symmetrically across apps/web + apps/mobile (or are legitimately platform-specific), i18n stays in sync, and no Critical/High issue survived adversarial verification.

Findings

  • Critical: None
  • High: None
  • Medium: None
  • Low/Info: None

Subagents

Agent Verdict
parity-checker PAIRED — the review-floor onMutate fix is legitimately mobile-only (Play Store in-app-review prompt has no web equivalent); the XP-row fix, pending-operation-card localization, and celebration unconditional-mount are mirrored on both platforms
i18n-syncer IN SYNC — 16 new chat.pendingOp.* keys + 2 changed gamification.profileCard.* keys present in both en.json/pt-BR.json, 0 missing, 0 orphaned callsites
contract-aligner N/A — diff doesn't touch packages/shared/src/types/* or endpoints.ts, and orbit-api isn't part of this diff
security-reviewer N/A — orbit-api not touched

Validation

Could not execute lint/type-check/tests in this review session (no interactive Bash approval available to this review agent). PR body self-reports mobile 930 ✓ / web 2105 ✓ / shared 1384 ✓, lint 0 errors, type-check clean — not independently re-run here.

What's good

  • apps/mobile/hooks/use-habits.ts: moving trackCompletion from onSuccess to onMutate is the correct fix — onMutate fires exactly once per user action regardless of whether the mutation resolves immediately, gets queued offline, or the queue is flushed later outside the mutation lifecycle. Mutations have retry: false (apps/mobile/lib/query-client.ts), so there's no double-count risk from retries. Good regression test pinning this against onMutate directly.
  • The Astra confirmation-card fix centralizes the capability-id → i18n-key and policy-reason → i18n-key mapping in one new shared util (packages/shared/src/utils/agent-pending-operation.ts), consumed identically by both platforms — unmapped ids/reasons fail safe (fallback to server displayName / generic error) instead of leaking raw codes.
  • apps/mobile/app/_layout.tsx: removing showSharedCelebrations = pathname !== '/' fixes a real footgun — it silently blocked every post-celebration prompt (including the new #382 review moment) on the one screen where completions actually happen, and now correctly matches web's existing unconditional mount.

Deferred

  • Dimension 8 (DESIGN.md/AI-slop): checked — the only UI-visual changes are gap/flexShrink spacing hardening on an existing row; nothing new to flag against the bans.
  • Dimensions 13 (backend hard rules) / 14 (FEATURES.md parity): N/A — orbit-api untouched; this is a bugfix/polish PR with no feature-inventory-level change.
  • The new LOCALIZED_CAPABILITY_IDS / LOCALIZED_POLICY_REASONS string literals in packages/shared/src/utils/agent-pending-operation.ts must match the real capabilityId/policyReason values orbit-api sends; that repo isn't checked out here so it can't be verified against source of truth. I investigated whether habits.delete (plural) might mismatch the real single-habit-delete capability id (pre-existing test fixtures elsewhere in this repo use singular habit.delete), but this repo's own packages/shared/src/types/config.ts plan-gating keys already establish habits.* plural as the in-repo domain-prefix convention, and the three habits.* entries in the new set share one consistent prefix — stronger evidence than the ambiguous placeholder fixtures — so I didn't carry this forward as a finding. A mismatch would degrade gracefully (English fallback) rather than break anything; a quick pt-BR sanity check on the "Delete habit" card is a cheap, non-blocking follow-up.

Recommendation

Nothing blocking. Optional follow-up: sanity-check the new capability-id/policy-reason string literals against the real orbit-api values in a manual pt-BR run, since there's no shared-type enforcement tying these hardcoded strings to the backend contract.

🤖 Generated with Claude Code /pr-review

@claude claude 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.

/pr-review — PR #403 (incremental)

Scope of this review: diff since my prior approvals at 6ee90b39 and a41d4088. Two new commits: 363820a2 (localize agent confirmation cards + stop leaking policy codes) and 6e73d1e9 (count bulk completions toward the review floor).

Recommendation: REQUEST CHANGES — one High finding.

High

Capability-id mapping likely uses the wrong naming convention — the localization fix may be a silent no-op in production

packages/shared/src/utils/agent-pending-operation.ts:1-14LOCALIZED_CAPABILITY_IDS uses plural-form ids for the simple CRUD capabilities: 'habits.delete', 'goals.delete', 'tags.delete', 'notifications.delete', 'user-facts.delete', 'subscriptions.manage', 'api-keys.manage', 'account.manage', 'sync.write', 'calendar.sync.manage'.

Every pre-existing capabilityId fixture in this repo — untouched by this PR — uses the singular form for the same field:

  • packages/shared/src/__tests__/types.test.ts:997agentCapabilitySchema fixture: id: 'habit.read' (note: same fixture uses plural domain/scope, singular id — the two are deliberately distinct conventions)
  • packages/shared/src/__tests__/types.test.ts:1055capabilityId: 'habit.delete'
  • packages/shared/src/__tests__/types.test.ts:1023relatedCapabilityIds: ['habit.read']
  • apps/web/__tests__/components/chat/pending-operation-card.test.tsx:16, apps/web/__tests__/components/chat/message-bubble.test.tsx:148, apps/mobile/__tests__/components/chat/pending-operation-card.test.tsx:58 — all default to capabilityId: 'habit.delete'

packages/shared/CLAUDE.md states these Zod schemas are "the contract with orbit-api" and that test factories are meant to be "realistic and minimal" — i.e. the singular-form fixtures are the closest in-repo proxy for the real API convention. If orbit-api's actual capability ids are singular (habit.delete, goal.delete, tag.delete, ...), none of this PR's ~10 plural CRUD entries will ever match a real capabilityId sent by the server.

Risk: getAgentCapabilityLabelKey() returns null for every real confirmation-gated operation, and the card silently keeps falling back to pendingOperation.displayName — the server's raw English string — which is exactly the bug this commit's message says it fixes. The new tests don't catch this because they exercise the mapping against capabilityId: 'habits.bulk.write', a value the PR itself invented and added to the same set, rather than against the repo's existing singular convention.

Fix: confirm the real orbit-api capabilityId strings for these capabilities and correct LOCALIZED_CAPABILITY_IDS to match (most likely singular for the CRUD entries — habit.delete, goal.delete, tag.delete, notification.delete, user-fact.delete, subscription.manage, api-key.manage, account.manage — keeping plural only where genuinely bulk-scoped, e.g. habits.bulk.write/habits.bulk.delete). Not verifiable in this CI job — orbit-api isn't checked out here — but a one-grep check against the sibling repo before merge.

Medium (non-blocking)

New shared util has no direct unit test

packages/shared/src/utils/agent-pending-operation.ts (new file, 2 exported functions) is only exercised indirectly through the mobile/web component test suites. Consider adding packages/shared/src/__tests__/agent-pending-operation.test.ts covering both functions' mapped/unmapped branches directly, so coverage doesn't silently erode if a future component change stops exercising every branch.

What's good

  • 6e73d1e9 (bulk-log review-floor counting) is a clean, correct fix that mirrors the already-reviewed useLogHabit.onMutate pattern exactly, with a matching regression test. No issues.
  • 363820a2's architecture is otherwise sound: one shared util consumed identically by both platforms, JSDoc on both exports, no any/console.log/comment-policy violations, i18n keys added to both en.json and pt-BR.json in the same diff with genuine translations, and resolveExecutionError now maps unknown policy reasons to a safe generic error instead of leaking raw server codes.
  • Parity and i18n verified in sync across both platforms for this diff.

Not verifiable in CI

  • contract-aligner (orbit-api capability-id convention) — sibling repo not checked out in this job; this is exactly what the High finding above depends on.
  • No orbit-api files changed in this diff, so the backward-compat guard and security-reviewer dimensions are N/A here.

Comment on lines +1 to +14
const LOCALIZED_CAPABILITY_IDS = new Set([
'habits.delete',
'habits.bulk.write',
'habits.bulk.delete',
'goals.delete',
'tags.delete',
'notifications.delete',
'calendar.sync.manage',
'user-facts.delete',
'subscriptions.manage',
'api-keys.manage',
'sync.write',
'account.manage',
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High: these ids are plural (habits.delete, goals.delete, tags.delete, notifications.delete, user-facts.delete, subscriptions.manage, api-keys.manage, account.manage, sync.write, calendar.sync.manage), but every pre-existing capabilityId fixture in this repo — untouched by this PR — uses the singular form for the same field: types.test.ts:997 (id: 'habit.read'), types.test.ts:1055 (capabilityId: 'habit.delete'), and the default fixtures in both platforms' pending-operation-card.test.tsx/message-bubble.test.tsx.

If orbit-api's real capability ids are singular, getAgentCapabilityLabelKey() returns null for every one of these in production, and the card silently keeps showing the server's raw English displayName — the exact bug this commit claims to fix. Please confirm the real orbit-api ids (singular for the CRUD ones, most likely) before merge.

thomasluizon and others added 2 commits July 6, 2026 13:44
Covers the mapped/unmapped branches of getAgentCapabilityLabelKey and
getAgentPolicyReasonKey directly (review Medium + Sonar new-code gate).

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

Copy link
Copy Markdown
Owner Author

Re the High finding (capability-id naming): verified against the authoritative source — the ids in LOCALIZED_CAPABILITY_IDS are copied verbatim from orbit-api src/Orbit.Domain/Models/AgentContracts.cs (AgentCapabilityIds), which uses the plural forms:

public const string HabitsDelete = "habits.delete";
public const string HabitsBulkWrite = "habits.bulk.write";
public const string GoalsDelete = "goals.delete";
public const string TagsDelete = "tags.delete";
public const string NotificationsDelete = "notifications.delete";
public const string CalendarSyncManage = "calendar.sync.manage";
public const string UserFactsDelete = "user-facts.delete";
public const string SubscriptionsManage = "subscriptions.manage";
public const string ApiKeysManage = "api-keys.manage";
public const string SyncWrite = "sync.write";
public const string AccountManage = "account.manage";

The singular habit.delete literals in the pre-existing client fixtures are stale invented values, not the contract — and they now conveniently exercise the unmapped-fallback branch. Empirical confirmation: the localized card ("Alterações de hábitos em lote") rendered correctly in live QA against the production API immediately after this change deployed to a device build.

The Medium (direct unit test for the shared util) is addressed in ce01e01, which also clears the Sonar new-code gate (the 20% figure was 2 uncovered lines out of 4 in that util — now covered directly).

@claude claude 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.

/pr-review — PR #403 (incremental)

Scope of this review: diff since my last CHANGES_REQUESTED review (which flagged one High + one Medium finding). Since then: one new commit, ce01e018 "test(shared): direct unit coverage for agent-pending-operation util", plus a merge of main.

Prior findings — resolved

  • High (capability-id naming convention) — the PR author responded with the authoritative source: orbit-api's src/Orbit.Domain/Models/AgentContracts.cs (AgentCapabilityIds) defines these ids in the plural form used by LOCALIZED_CAPABILITY_IDS (HabitsDelete = "habits.delete", GoalsDelete = "goals.delete", etc.) — the singular-form fixtures elsewhere in this repo are stale/invented values, not the real contract. This was empirically confirmed in a live QA pass against the production API (localized "Alterações de hábitos em lote" rendered correctly). Resolved.
  • Medium (no direct unit test for the shared util)ce01e018 adds packages/shared/src/__tests__/agent-pending-operation.test.ts, covering both getAgentCapabilityLabelKey and getAgentPolicyReasonKey across their mapped and unmapped/null/empty branches directly. Matches the util's actual behavior (verified by reading packages/shared/src/utils/agent-pending-operation.ts). Resolved, and clears the Sonar new-code coverage gate cited in the author's follow-up comment.

New findings

None. No Critical/High/actionable-Medium issues in this incremental diff.

Not verifiable in CI

  • Cross-repo confirmation of the capability-id convention rests on the author's cited orbit-api source, which isn't checked out in this job — noted as author-provided evidence, not independently re-verified here.

Recommendation: Approve

All findings from the prior review round are resolved — one via authoritative clarification, one via an added test. Nothing new to flag.

@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

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.

1 participant