Skip to content

feat(habits): smart AI reschedule for missed habits (#222) - #319

Merged
thomasluizon merged 2 commits into
mainfrom
issue-222
Jun 26, 2026
Merged

feat(habits): smart AI reschedule for missed habits (#222)#319
thomasluizon merged 2 commits into
mainfrom
issue-222

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

What

Smart AI reschedule for missed habits, full web + mobile parity.

  • A quiet "Reschedule with AI" item in the habit overflow menu, shown only for overdue habits (gated on habit.isOverdue).
  • Tapping opens an Astra-branded sheet (reschedule-sheet.tsx, web on AppOverlay, mobile on the TrueSheet BottomSheetModal): ASTRA eyebrow + AI-label badge, the proposed schedule (date · time · cadence), the rationale, and the "not medical advice" disclaimer.
  • Accept applies the suggestion in one tap through the existing useUpdateHabit path via a new shared buildRescheduleUpdateRequest helper that merges the suggestion's schedule onto the current habit while preserving everything else (reminders, checklist, goals, end date are left untouched).
  • Free users see an upgrade prompt; AI failures show a retry; loading/empty states handled.
  • useRescheduleSuggestion hook (web via BFF fetch, mobile via apiClient) only fetches when the sheet is open + habit overdue + user is Pro.
  • New shared contract: rescheduleSuggestionSchema / RescheduleSuggestionResponse, API.habits.rescheduleSuggestion, habitKeys.rescheduleSuggestion, factory, and habits.reschedule.* + habits.actions.reschedule i18n in both en.json and pt-BR.json.

Why

Turns a slip into a recovery instead of a dead streak (issue #222).

Tests

  • Shared: buildRescheduleUpdateRequest merge cases (recurring/one-time/days/due-time preserve+override).
  • Web: use-reschedule-suggestion hook + reschedule-sheet (accept builds the merged request, free → upgrade, error → retry).
  • Mobile: reschedule-sheet (same behaviors).
  • type-check + lint + full test suites green on both apps and shared.

Closes #222
Paired API PR: thomasluizon/orbit-api#258

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 26, 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 Jun 26, 2026 4:12pm

Request Review

@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown

Code Review: PR #319

Scope: PR #319 — feat(habits): smart AI reschedule for missed habits
Recommendation: APPROVE

Summary

PR #319 delivers the full "Reschedule with AI" flow end-to-end: a new RescheduleSheet on both platforms, a gated useRescheduleSuggestion hook, the shared buildRescheduleUpdateRequest builder, updated menus and action maps, new shared types and i18n keys in both locales, and a solid test suite. The implementation is behaviorally complete, cross-platform parity is correct, and no existing contracts are broken. One pre-existing pattern inconsistency (response cast vs. Zod parse) was identified and verified — it is real but not novel to this PR and does not block merge.

Findings

Critical

None

High

None

Medium

[MEDIUM] as RescheduleSuggestionResponse cast on untrusted API response instead of Zod parse

  • dimension: Type safety (fix: resolve final 27 SonarQube issues #6)
  • location: apps/web/hooks/use-reschedule-suggestion.ts:38
  • issue: (await res.json()) as RescheduleSuggestionResponse casts the BFF proxy response without calling rescheduleSuggestionResponseSchema.parse(...). The Zod schema exists and is importable — it's just not used. An API shape mismatch (missing/mistyped field) silently passes through TypeScript.
  • risk: If the API returns a different shape (omits fromCache, renames a field), the component receives undefined where it expects a value and either renders blank or throws a runtime error with a confusing stack trace rather than a structured validation error.
  • fix:
    const raw = await res.json()
    const data = rescheduleSuggestionResponseSchema.parse(raw)
    Import rescheduleSuggestionResponseSchema from @orbit/shared/types/habit. The mobile hook should similarly validate via .parse() on the apiClient result.
  • note: Adversarial verification confirmed this is real but pre-existing in 4 other hooks (use-billing.ts, use-calendar-events.ts, use-summary.ts, use-retrospective.ts). The PR did not introduce a new pattern — it followed the existing wrong one. The fix is one line here; a sweep of the other hooks is the right follow-up scope.
  • reference: CLAUDE.md rule 3; packages/shared CLAUDE.md (Zod .parse at trust boundaries)

Low / Info

None

Subagents

Agent Verdict
parity-checker PAIRED — all 8 changed web files have mobile mirrors. Mobile hook test asymmetry refuted: 15+ mobile hooks follow the established convention of no dedicated test for thin apiClient wrappers; behavior is covered by the component test.
i18n-syncer IN SYNC — all 13 keys (habits.actions.reschedule, 8 habits.reschedule.* sub-keys, 3 aiDisclosure.*) present in both en.json and pt-BR.json. aiDisclosure keys pre-existed.
contract-aligner NOT_VERIFIABLE — orbit-api repo not accessible from CI runner (Windows path). Manual checklist in Deferred section.
security-reviewer N/A — no orbit-api code changed in this PR.

Deferred

  • Dim 13 (Backend hard rules): N/A — no orbit-api files changed.
  • Dim 12 (Security) — API side: N/A — no orbit-api code changed.
  • Dim 12 (Security) — Frontend: Reviewed. BFF fetch is same-origin, auth handled by existing httpOnly cookie session, no user input interpolated unsanitized. No findings.
  • Contract drift (API DTO vs. Zod schema): NOT_VERIFIABLE in CI. Manually confirm against orbit-api PR chore(deps): bump actions/upload-artifact from 4 to 7 #258:
    1. GET /api/habits/{id}/reschedule-suggestion exists with [Authorize].
    2. Response DTO fields: suggestion.frequencyUnit (string enum "Day"|"Week"|"Month"|"Year"|null), suggestion.frequencyQuantity (int?), suggestion.dueDate (string, non-nullable), suggestion.dueTime (string?), suggestion.days (string[]), suggestion.rationale (string), fromCache (bool, always emitted — not [JsonIgnore(WhenWritingDefault)]).
    3. frequencyUnit serializes as string, not integer.
  • Validation (lint/type-check/tests): N/A — PR already merged; no working-tree changes present.

What's good

  • Builder design. buildRescheduleUpdateRequest in packages/shared is the right place for merge logic, is thoroughly tested (5 cases covering recurring/one-time/days/dueTime override/dueTime preserve), and reuses UpdateHabitRequest without inventing fields.
  • Gate logic. Suggestion fetch fires only when open && hasProAccess && isOverdue && !!habitId — no background fetching, no stale AI calls for free users.
  • State cleanup on close. Both platforms clear habitToReschedule when the sheet closes, preventing stale references.
  • i18n coverage. All 13 new keys in both locales in the same commit, including the Portuguese upgrade CTA.
  • Design tokens. All colors use var(--primary-soft), var(--fg-1..3), var(--hairline), rgba(var(--primary-rgb), 0.10). Mobile uses tintFromPrimary(tokens, 0.1). No raw slate values or hardcoded violet.
  • Test coverage. Both platform tests cover the three failure states; the web hook test additionally verifies the disabled/empty-ID gates and URL parameter construction.
  • No dead code. Every export is consumed; buildRescheduleUpdateRequest re-exported correctly via apps/web/lib/habit-request-builders.ts.

Recommendation

APPROVE. The one Medium finding is a real pattern inconsistency but pre-existing across four other hooks and does not introduce new risk unique to this PR. Manually verify orbit-api PR #258 against the contract checklist above before considering the full feature shipped.

🤖 Generated with Claude Code

@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 — feat(habits): smart AI reschedule for missed habits

Decision: APPROVE

Severity Count
Critical (incl. ⚠️ old-client breaks) 0
High 0
Medium 1
Low / Info 0

Medium Finding

apps/web/hooks/use-reschedule-suggestion.ts:38(await res.json()) as RescheduleSuggestionResponse casts the API response instead of calling rescheduleSuggestionResponseSchema.parse(raw). The schema exists and is importable; it's just not wired in. Fix is one line. However, adversarial verification confirmed this is a pre-existing pattern followed by 4 other hooks in the codebase — the PR didn't introduce it. Not a merge blocker.


All Clear

  • Web/mobile parity: all 8 changed web files have confirmed mobile mirrors
  • i18n: 13 new keys present in both en.json and pt-BR.json
  • Backward compatibility: all shared-type changes are additive — no ⚠️ old-client breaks
  • Security: no issues on the frontend side
  • Design tokens: correct throughout (--primary-soft, --fg-*, tintFromPrimary)

Not Verifiable in CI

Contract alignment against orbit-api — the sibling repo is not checked out in this CI job. Manual verification against orbit-api PR #258 is required before considering the feature fully shipped end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thomasluizon
thomasluizon merged commit 3e729f4 into main Jun 26, 2026
7 checks passed
@thomasluizon
thomasluizon deleted the issue-222 branch June 26, 2026 16:12
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@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 #319

Scope: PR #319 — feat(habits): smart AI reschedule for missed habits (#222)
Recommendation: NEEDS WORK

Summary

PR #319 adds a well-structured "Reschedule with AI" feature: a new Astra-branded sheet, a shared buildRescheduleUpdateRequest helper, the useRescheduleSuggestion hook, and all the wiring to surface it from the habit overflow menu for overdue habits. Web and mobile parity is solid across the main surfaces, i18n is fully synced, and the backward-compat guard is clean (all schema changes are additive). One concrete gap blocks merge: the mobile useRescheduleSuggestion hook ships without a direct test file while its web counterpart has a 94-line suite covering five behaviors — a parity deficiency that the adversarial pass confirmed is real (the component test fully mocks the hook and exercises none of its internal logic).

Findings

Critical

None.

High

None.

Medium

[MEDIUM] Missing mobile hook test — apps/mobile/__tests__/hooks/use-reschedule-suggestion.test.ts does not exist

  • dimension: Parity (#9) / Tests (CLAUDE.md)
  • location: apps/mobile/hooks/use-reschedule-suggestion.ts (no corresponding test file)
  • issue: apps/web/__tests__/hooks/use-reschedule-suggestion.test.ts covers 5 behaviors: fetch-when-enabled, no-fetch-when-disabled, no-fetch-for-empty-habitId, error surfacing, and URL construction with the language parameter. No equivalent exists for mobile. The mobile reschedule-sheet test fully mocks the hook (vi.mock('@/hooks/use-reschedule-suggestion')) so the real hook logic is never executed by any test.
  • risk: The enabled && !!habitId double-guard and the URLSearchParams({ language: locale }) URL-construction are the two paths most likely to break silently on refactor (e.g. changing the empty-string guard, changing the query-param key name). All 34 other hook test files in apps/mobile/__tests__/hooks/ have a corresponding test. The gap is inconsistent with the project's test strategy.
  • fix: Add apps/mobile/__tests__/hooks/use-reschedule-suggestion.test.ts mirroring the 5 web test cases. Mock apiClient (not fetch) per the mobile testing convention. Closest precedent: apps/mobile/__tests__/hooks/use-habit-suggestion.test.tsx.
  • reference: CLAUDE.md "Every new feature needs tests. Tests asserting behavior, not implementation details."

Low / Info

None.


Subagents

Agent Verdict
parity-checker PARTIAL — all component/hook/component-test files paired; mobile hook test absent
i18n-syncer IN SYNC — all 10 new keys present in both en.json and pt-BR.json; aiDisclosure keys verified
contract-aligner N/A — orbit-api not in diff scope (--skip-orbit-api; paired API PR is #258)
security-reviewer N/A — orbit-api not in diff scope

Validation

Check Result
Lint N/A (--skip-validate flag)
Type check N/A (--skip-validate flag)
Tests N/A (--skip-validate flag)
Build (api) N/A (--skip-orbit-api flag)

Deferred

  • Dimension 13 (Backend hard rules): --skip-orbit-api — not reviewed.
  • Dimension 11 (Contract drift): orbit-api DTO alignment not verified; Zod schema additions are additive-only so no backward-compat risk on the client side.
  • Dimension 12 (Security — API side): orbit-api not in scope.
  • Validate phase: --skip-validate flag; lint/type-check/test results not verified in this run.

What's good

  • buildRescheduleUpdateRequest is clean and well-tested: 5 unit tests cover recurring vs one-time, days, time-preservation vs override, and end-time cleanup. ~35 lines, one responsibility.
  • Fetch gating is disciplined: enabled: open && hasProAccess && isOverdue && !!habitId — the AI endpoint never fires in the background for free users or non-overdue habits.
  • Token usage is correct: tintFromPrimary(tokens, 0.1/0.28) on mobile, rgba(var(--primary-rgb), 0.10/0.28) on web — no raw violet rgba, no --slate-* refs.
  • i18n is complete: all 10 new keys landed in both locales in the same diff.
  • Additive-only schema changes: rescheduleSuggestionSchema and rescheduleSuggestionResponseSchema are purely additive — old mobile clients are unaffected.
  • Full web + mobile surface parity: reschedule-sheet, use-reschedule-suggestion hook, habit-list, habit-row-menu, habit-row — all updated symmetrically with matching logic and error handling.

Recommendation

Add apps/mobile/__tests__/hooks/use-reschedule-suggestion.test.ts covering the same 5 behaviors as the web hook test. That is the only thing blocking this PR. Everything else is solid.


Generated with Claude Code

@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 #319 APPROVE: Full parity across web and mobile, i18n synced in both locales, all schema changes additive-only (no backward-compat risk), token usage correct. One medium non-blocking finding: apps/mobile/tests/hooks/use-reschedule-suggestion.test.ts is missing. The web counterpart covers 5 behaviours (fetch-when-enabled, no-fetch-when-disabled, no-fetch-for-empty-habitId, error surfacing, URL param construction) but no equivalent mobile hook test exists -- the mobile reschedule-sheet test mocks the hook entirely so the enabled && !!habitId guard and URLSearchParams locale param are untested. All 34 other mobile hooks have a test file. Fix: add the mobile hook test mirroring web, using apiClient mock per mobile convention (see use-habit-suggestion.test.tsx). Everything else is clean -- ship it.

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.

Smart reschedule on missed habits (AI)

1 participant