Skip to content

MVP: תשתית FocusBalance עם Dexie, Tailwind, וניווט#1

Merged
amirbiron merged 6 commits into
mainfrom
claude/implement-focusbalance-docs-AJFrA
May 16, 2026
Merged

MVP: תשתית FocusBalance עם Dexie, Tailwind, וניווט#1
amirbiron merged 6 commits into
mainfrom
claude/implement-focusbalance-docs-AJFrA

Conversation

@amirbiron
Copy link
Copy Markdown
Owner

@amirbiron amirbiron commented May 16, 2026

סיכום

הקמת תשתית MVP מלאה של FocusBalance — אפליקציית ווב לניהול והפחתה של מינון ריטלין. כל הנתונים נשמרים מקומית ב-IndexedDB (Dexie), ללא שליחה לשרת. הפרויקט כולל מודל נתונים מלא, ניווט תחתון, FAB לתיעוד מהיר, ו-6 עמודים stub.

שינויים עיקריים

מודל נתונים וDB

  • src/db/schema.ts – הגדרת TypeScript interfaces לכל הישויות: User, DoseEvent, Trigger, ReductionPlan
  • src/db/database.ts – מופע Dexie עם 4 טבלאות, אינדקסים לסינון מהיר, ו-LOCAL_USER_ID = 1 (משתמש יחיד ב-MVP)
  • src/db/seed.ts – טעינת 13 טריגרים ברירת-מחדל (משימה, קוגניטיבי, פיזי, רגשי, הרגל) בטרנזקציה אטומית (כלל 2)
  • src/db/repositories/ – שלוש repositories:
    • users.tsgetLocalUser(), upsertLocalUser(), acceptDisclaimer()
    • doses.tsaddDoseEvent(), getDoseEventsInRange(), getTodayDoseEvents(), deleteDoseEvent(), updateDoseEvent()
    • triggers.tslistTriggers(), listTriggersByCategory(), addCustomTrigger()

ולידציה וביטחון

  • src/lib/utils.tsisValidPositiveNumber() – בדיקת NaN, Infinity, וטווח (כלל 4)
  • כל repository מאמת קלט מספרי לפני שמירה ל-DB

עיצוב וסטיילינג

  • tailwind.config.js – פלטה מותאמת: סגול עמוק (brand), טורקיז (accent), כתום חם (warm), עם צלליות רכות וגופנים עברים (Heebo, Assistant)
  • src/styles/index.css – CSS בסיסי: רקע מנוקד, פוקוס נגיש, component classes (.card, .btn-primary, .btn-ghost, .chip)

Layout וניווט

  • src/components/layout/AppLayout.tsx – מבנה הדף: TopBar, main content, FAB, BottomNav, Footer
  • src/components/layout/TopBar.tsx – כותרת עם לוגו ושם האפליקציה
  • src/components/layout/BottomNav.tsx – ניווט תחתון עם 5 קישורים (בית, תובנות, תוכנית, יומן, הגדרות)
  • src/components/layout/QuickLogFab.tsx – כפתור FAB כתום לתיעוד מהיר (מחובר ל-Zustand store)
  • src/components/layout/Footer.tsx – קישור עדין ל"מתי לפנות לעזרה

https://claude.ai/code/session_01FjJS1YsCyHPNyfQz6nuZBk

Summary by CodeRabbit

  • New Features

    • Full FocusBalance MVP: dashboard (Today, Streak, Weekly chart, Insight cards), insights, journal, plan, settings, help, and 3‑step onboarding (welcome → disclaimer → profile).
    • Hebrew‑first RTL UI with responsive mobile layout (top bar, bottom nav, footer) and global Tailwind styles.
    • Quick Log FAB + multi‑step Quick Log modal for fast dose entries, local IndexedDB storage with seeded triggers.
    • Profile form/settings with local persistence, accessible UI components, and an emergency help page with callable contacts.
  • Documentation

    • Hebrew README with non‑medical disclaimer, privacy note (local storage only), tech overview and dev commands.

Review Change Stack

הקמת בסיס הפרויקט לפי האפיון ב-docs/FocusBalance.md:

- שלב 0: Vite + React 18 + TypeScript + Tailwind 3, פלטה ייעודית
  (סגול #6B46C1, טורקיז #0D9488, כתום חם #EA580C — בלי אדום),
  גופן Heebo/Assistant, RTL מלא, ESLint, alias @/* ל-src.
- שלב 1: מודל נתונים מלא לפי סעיף 5 (User, DoseEvent, Trigger,
  ReductionPlan), Dexie + IndexedDB עם אינדקסים מורכבים,
  repositories עם ולידציית NaN/Inf (כלל 4 ב-CLAUDE.md),
  seed אטומי של 15 טריגרים ברירת-מחדל ב-6 קטגוריות (כלל 2).
- שלב 2: AppLayout מובייל-first עם ניווט תחתון ל-5 מסכים,
  FAB גלובלי לתיעוד מהיר (Zustand store), פוטר עם קישור
  קבוע ל"מתי לפנות לעזרה" (דרישה מסעיף 9), 6 מסכים stub,
  עמוד 404, עמוד עזרה עם מספרי חירום מבודדים LTR.

הכל מקומי ב-IndexedDB — בלי backend, בלי שליחה לצד ג'
(עיקרון הפרטיות מסעיף 8 באפיון).

typecheck וגם build עוברים נקי. dev server מגיב 200.

https://claude.ai/code/session_01FjJS1YsCyHPNyfQz6nuZBk
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

FocusBalance introduces a Hebrew-first React+Vite app: project config and tooling, Tailwind design system, Dexie DB (schema, repositories, seeding), UI primitives and modal, onboarding and quick-log flows, dashboard cards/charts, pages/routing, and local-only IndexedDB storage.

Changes

FocusBalance MVP - New React App

Layer / File(s) Summary
Project Configuration & Build Tools
.eslintrc.cjs, .gitignore, index.html, package.json, postcss.config.js, tsconfig.json, vite.config.ts
ESLint config, .gitignore, HTML entry, package scripts & deps, PostCSS (Tailwind + autoprefixer), strict TS config, and Vite config with @ alias.
Design System - Tailwind & CSS
tailwind.config.js, src/styles/index.css
Tailwind content globs, custom color/shadow/radius tokens, Hebrew font stack, global base styles and component utilities, and RTL icon mirror rule.
Database Schema & Utilities
src/db/schema.ts, src/lib/utils.ts
Domain types (TriggerCategory, User, DoseEvent, Trigger, ReductionPlan); cn() class helper and isValidPositiveNumber() guard.
Database Layer - Dexie & Seeding
src/db/database.ts, src/db/seed.ts
Dexie FocusBalanceDB with versioned stores (users, doseEvents, triggers, reductionPlans), exported singleton db, LOCAL_USER_ID, and seedIfNeeded() to populate system triggers.
Repository Functions - CRUD Operations
src/db/repositories/users.ts, src/db/repositories/doses.ts, src/db/repositories/triggers.ts, src/hooks/useLocalUser.ts
User repo: getLocalUser, upsertLocalUser, acceptDisclaimer; Dose repo: add/get/update/delete dose events with validation and range/today/getRecentDays queries; Trigger repo: list, group by category, addCustomTrigger with label validation; useLocalUser subscribes to local user.
Global State Store
src/store/quickLogStore.ts
Zustand store useQuickLogStore controlling Quick Log modal isOpen state with open/close actions.
UI Primitives
src/components/ui/Button.tsx, src/components/ui/Input.tsx, src/components/ui/Modal.tsx
Accessible Button and Input primitives (forwardRef, aria wiring), and a portal Modal with escape handling and scroll lock.
Layout Components & Navigation
src/components/layout/TopBar.tsx, src/components/layout/BottomNav.tsx, src/components/layout/Footer.tsx, src/components/layout/QuickLogFab.tsx, src/components/layout/AppLayout.tsx
TopBar brand/link, BottomNav with NavLinks/icons, Footer help link, QuickLogFab wired to store, and AppLayout composing header, Outlet, QuickLogModal, and bottom controls.
Onboarding Flow & Steps
src/pages/OnboardingPage.tsx, src/components/onboarding/WelcomeStep.tsx, src/components/onboarding/DisclaimerStep.tsx, src/components/onboarding/ProfileStep.tsx
Three-step onboarding (welcome → disclaimer → profile) with validation and persistence; profile upserts local user then accepts disclaimer and navigates to /.
Quick Log Modal
src/components/quicklog/QuickLogModal.tsx
Multi-step quick logging flow: now/custom time, amount selection/validation, trigger selection, persistence of dose event, and done confirmation showing exceed status.
Settings — ProfileForm
src/components/settings/ProfileForm.tsx, src/pages/SettingsPage.tsx
ProfileForm validates/saves prescribed dose, unit doses, scheduled times, optional doctor name; SettingsPage wires ProfileForm to upsertLocalUser and shows privacy text about local IndexedDB storage.
Dashboard Components & Charts
src/components/dashboard/*.tsx, src/pages/DashboardPage.tsx
TodayCard, StreakCard, WeeklyChart, InsightCard use live Dexie queries and helpers to aggregate/visualize weekly and daily metrics; DashboardPage composes these cards.
Pages & Application Routing
src/App.tsx, src/main.tsx, src/pages/*.tsx
main.tsx bootstraps BrowserRouter + App; App runs seedIfNeeded() and registers guarded /onboarding plus routes for dashboard, insights, plan, journal, settings, help, and not-found.
Project Documentation
README.md
Hebrew README with non-medical disclaimer, tech stack, dev/build/typecheck/lint commands, src/ structure, MVP checklist, and privacy guarantee.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant main_tsx as main.tsx
  participant App
  participant seedIfNeeded
  participant db_triggers as db.triggers
  participant Routes
  participant Onboarding
  participant AppLayout

  Browser->>main_tsx: load /src/main.tsx
  main_tsx->>App: mount with BrowserRouter
  App->>seedIfNeeded: call on mount
  seedIfNeeded->>db_triggers: check count
  alt triggers empty
    seedIfNeeded->>db_triggers: bulkAdd SYSTEM_TRIGGERS
  end
  seedIfNeeded-->>App: resolve (isReady=true)
  App->>Routes: render Routes
  alt user missing disclaimer
    Routes->>Onboarding: render /onboarding (Welcome → Disclaimer → Profile)
  else user onboarded
    Routes->>AppLayout: render main routes (Dashboard, Insights, Plan, Journal, Settings, Help)
  end
  AppLayout->>AppLayout: render TopBar + Outlet + BottomNav + QuickLogFab + Footer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through fonts and Dexie seeds with glee,
RTL Hebrew frames and cards for all to see,
Quick logs and charts, a modal bright and neat,
Local data safe where browser memories meet,
FocusBalance blooms — soft, kind, and ready to be.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changeset: establishing the MVP foundation for FocusBalance with Dexie, Tailwind, and navigation, which aligns with the substantial infrastructure additions documented in the raw summary.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/implement-focusbalance-docs-AJFrA

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

Comment thread src/db/repositories/doses.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 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 @.gitignore:
- Around line 21-24: The .gitignore currently only ignores .env, .env.local and
.env.*.local and can miss other variants like .env.production or .env.staging;
update the ignore rules by replacing/augmenting those lines with a broader
pattern (e.g., add ".env.*" or ".env*") and explicitly re-include the example
file by adding an exception rule "!.env.example" so .env.* files are ignored but
.env.example remains tracked; adjust the entries around the existing ".env",
".env.local", and ".env.*.local" lines accordingly.

In `@index.html`:
- Around line 8-13: Remove the external Google Fonts preconnect and stylesheet
links (the <link rel="preconnect" href="https://fonts.googleapis.com">, <link
rel="preconnect" href="https://fonts.gstatic.com" crossorigin>, and the <link
href="https://fonts.googleapis.com/css2?family=Heebo...&family=Assistant...&display=swap"
rel="stylesheet"> entries) and replace them with self-hosted font files: add
Heebo and Assistant font files to your static assets (e.g., assets/fonts/),
create `@font-face` rules in your CSS that reference those local files
(woff2/woff), and update any font-family usages to use "Heebo" and "Assistant"
as defined in the `@font-face` blocks so browsers load fonts locally without
contacting Google.

In `@README.md`:
- Around line 30-43: The Markdown code fence in README.md that shows the folder
structure lacks a language tag (causing markdownlint MD040); update the opening
triple-backtick for that code block to include a language identifier (e.g.,
change ``` to ```text) so the block becomes ```text and re-run linting to
confirm the warning is resolved.

In `@src/db/repositories/doses.ts`:
- Around line 51-58: updateDoseEvent currently validates amountMg but not the
event timestamp, allowing invalid timestamps; add the same timestamp check used
in addDoseEvent (validate patch.timestamp with the existing timestamp validation
helper, e.g., isValidTimestamp or the same logic used in addDoseEvent) and throw
the same error if invalid, before calling db.doseEvents.update(id, patch).
Ensure you reference the patch.timestamp field and keep the existing amountMg
validation intact so both validations run in updateDoseEvent.

In `@src/db/repositories/users.ts`:
- Around line 14-25: upsertLocalUser currently validates doses but never checks
the scheduledTimes format, allowing invalid "HH:MM" values to be persisted; add
validation inside upsertLocalUser that iterates data.scheduledTimes (and handles
missing/empty) and asserts each entry matches a strict 24-hour "HH:MM" pattern
(e.g. regex for hours 00–23 and minutes 00–59) or parse-equivalent, and throw a
descriptive Error (e.g. 'scheduledTimes אינו בפורמט HH:MM') when any entry is
invalid so invalid times cannot be saved.

In `@src/styles/index.css`:
- Around line 12-14: Add a blank line before the `font-size: 16px;` declaration
in src/styles/index.css to satisfy the Stylelint rule
`declaration-empty-line-before`; locate the rule block where `@apply bg-surface
text-slate-900 antialiased;` and `line-height: 1.7;` are defined and insert an
empty line immediately above the `font-size` declaration so the declarations are
separated per `declaration-empty-line-before`.
- Around line 1-3: Stylelint is flagging valid Tailwind at-rules (`@tailwind`,
`@layer`, `@apply`) as unknown; update your Stylelint config (e.g., .stylelintrc.js
/ package.json stylelint section) to add an ignoreAtRules entry including
"tailwind", "apply", "layer" (or the full at-rule names) so the
at-rule-no-unknown rule ignores these directives; ensure the change is applied
where at-rule-no-unknown is inherited (from stylelint-config-standard-scss) so
the `@tailwind`, `@layer`, and `@apply` directives in src/styles/index.css are no
longer reported.
🪄 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 Plus

Run ID: 4894a43d-8133-4287-9bc8-4836e6f7b91f

📥 Commits

Reviewing files that changed from the base of the PR and between 2722060 and 462045f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (32)
  • .eslintrc.cjs
  • .gitignore
  • README.md
  • index.html
  • package.json
  • postcss.config.js
  • src/App.tsx
  • src/components/layout/AppLayout.tsx
  • src/components/layout/BottomNav.tsx
  • src/components/layout/Footer.tsx
  • src/components/layout/QuickLogFab.tsx
  • src/components/layout/TopBar.tsx
  • src/db/database.ts
  • src/db/repositories/doses.ts
  • src/db/repositories/triggers.ts
  • src/db/repositories/users.ts
  • src/db/schema.ts
  • src/db/seed.ts
  • src/lib/utils.ts
  • src/main.tsx
  • src/pages/DashboardPage.tsx
  • src/pages/HelpPage.tsx
  • src/pages/InsightsPage.tsx
  • src/pages/JournalPage.tsx
  • src/pages/NotFoundPage.tsx
  • src/pages/PlanPage.tsx
  • src/pages/SettingsPage.tsx
  • src/store/quickLogStore.ts
  • src/styles/index.css
  • tailwind.config.js
  • tsconfig.json
  • vite.config.ts

Comment thread .gitignore
Comment thread index.html
Comment thread README.md
Comment thread src/db/repositories/doses.ts Outdated
Comment thread src/db/repositories/users.ts
Comment thread src/styles/index.css
Comment thread src/styles/index.css
מימוש סעיפים 4.6 ו-9 באפיון:

- מסך אונבורדינג בן 3 שלבים (Welcome → Disclaimer → Profile)
  עם מחוון התקדמות.
- מסך disclaimer מפורש: הצהרה שזה לא כלי רפואי, התחייבות
  להתייעצות עם רופא, מספרי חירום (מד״א/ער״ן) ב-LTR מבודד,
  וצ׳קבוקס שמחייב הסכמה לפני מעבר הלאה.
- טופס פרופיל רפואי: מינון מרשם, מערך מינוני יחידה (דינמי
  עם הוספה/הסרה), מערך שעות מתוכננות (time picker עם
  dir="ltr"), שם רופא אופציונלי. ולידציית NaN/Infinity מלאה
  לפי כלל 4, deduplication של ערכים, מיון.
- Route guard ב-App.tsx: כניסה לכל מסך פנימי דורשת
  hasAcceptedDisclaimer === true; אחרת ניתוב ל-/onboarding.
  המסלול ההפוך מנותב חזרה הביתה אם המשתמש כבר עבר אונבורדינג.
- עמוד הגדרות אמיתי לעריכת הפרופיל בכל עת + הודעת "נשמר".
- Hook ראקטיבי useLocalUser המבוסס על dexie-react-hooks
  לסנכרון אוטומטי בכל שינוי במסד.
- רכיבי UI בסיסיים לשימוש חוזר: Input (עם תווית, רמז,
  שגיאה ו-aria-describedby) ו-Button (variants + sizes).

typecheck וbuild נקיים; dev server מחזיר 200 גם ל-/ וגם
ל-/onboarding.

https://claude.ai/code/session_01FjJS1YsCyHPNyfQz6nuZBk
Comment thread src/components/onboarding/ProfileStep.tsx
מימוש סעיף 4.2 באפיון. המודל נפתח מה-FAB ומחזיק 4 שלבים
עם מחוון התקדמות:

- שלב 1 (מתי?): כפתור "עכשיו" ברירת-מחדל מול time picker
  בכיוון LTR; השעה ממומשת על תאריך היום.
- שלב 2 (כמה?): chips של unitDoses מהפרופיל + שדה ידני.
  ולידציית NaN/Infinity לפני אישור (כלל 4 ב-CLAUDE.md).
- שלב 3 (למה?): טריגרים מקובצים ל-6 קטגוריות עם אייקונים,
  ניתן לבחור מספר. אפשרות לדלג לחלוטין. שדה הערה אופציונלי.
- שלב 4 (אישור): מסך "תועד" עם שני וריאנטים — סטנדרטי
  (ירוק-טורקיז עם ✓) או הודעה חמלתית כשמעבר ליעד (סגול
  עם 💜 וטקסט "ננתח את זה ביחד מחר"), בלי אדום. מציג סכום
  היום מול היעד.

תוספות תומכות:
- Modal נגיש כללי (Escape, נעילת גלילה, b-sheet במובייל).
- doseHelpers: sumAmountMg עם הגנה מ-NaN, isPlannedDose עם
  סבילות ±60 דק' מול scheduledTimes, wouldExceedDailyTarget,
  והמרת HH:MM של היום ל-ISO.
- חיבור QuickLogModal ל-AppLayout כדי שיהיה זמין מכל מסך.

typecheck + build נקיים, dev server מחזיר 200.

https://claude.ai/code/session_01FjJS1YsCyHPNyfQz6nuZBk
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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/components/onboarding/ProfileStep.tsx`:
- Around line 84-96: setIsSaving is only reset inside the catch, so after a
successful await onSubmit the saving state remains true and the form stays
locked; update the submit flow in ProfileStep.tsx to always clear the saving
flag by moving setIsSaving(false) into a finally block (or call it immediately
after the awaited onSubmit succeeds) so that setIsSaving(false) runs regardless
of success or error; keep existing error handling (setErrors) and retain the
call to onSubmit with prescribedDailyDose/unitDoses/scheduledTimes/doctorName.

In `@src/components/settings/ProfileForm.tsx`:
- Line 1: In ProfileForm.tsx, clear any existing success timeout before creating
a new one and ensure cleanup on unmount: add a ref (e.g., successTimeoutRef) to
hold the timeout id used when you call setShowSaved / setSaved (or inside
handleSave/handleSaveSuccess), call clearTimeout(successTimeoutRef.current)
before assigning a new setTimeout, store the id into successTimeoutRef.current,
and add a useEffect cleanup that clears the timeout on unmount to prevent stale
timers and premature hiding of the "נשמר" message.

In `@src/components/ui/Input.tsx`:
- Around line 14-15: The aria-describedby attribute references hintId even when
the hint element is not rendered due to the `hint && !error` check; update the
logic so described IDs are assembled only for actually rendered elements:
compute describedBy (or the aria-describedby value) from errorId (if error) and
hintId only when hint && !error, or move creation of hintId to be conditional
with the same `hint && !error` predicate; ensure the input's aria-describedby
includes only IDs that will be rendered (refer to hintId, errorId, inputId in
Input.tsx and the place where aria-describedby is set).
- Line 13: The fallback ID generation for the Input component (inputId = id ??
`input-${Math.random()...}`) is unstable and should be replaced with React's
useId(); import useId from React, call it inside the Input component (e.g.,
const generatedId = useId()) and set inputId = id ?? generatedId so the ID is
stable across SSR/hydration and re-renders; ensure all places that reference
inputId (label htmlFor, aria-describedby, etc.) keep using the new inputId
variable.
🪄 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 Plus

Run ID: c5997692-3b57-4643-b2e1-7f3f83a60c22

📥 Commits

Reviewing files that changed from the base of the PR and between 462045f and 0a47931.

📒 Files selected for processing (10)
  • src/App.tsx
  • src/components/onboarding/DisclaimerStep.tsx
  • src/components/onboarding/ProfileStep.tsx
  • src/components/onboarding/WelcomeStep.tsx
  • src/components/settings/ProfileForm.tsx
  • src/components/ui/Button.tsx
  • src/components/ui/Input.tsx
  • src/hooks/useLocalUser.ts
  • src/pages/OnboardingPage.tsx
  • src/pages/SettingsPage.tsx

Comment thread src/components/onboarding/ProfileStep.tsx
Comment thread src/components/settings/ProfileForm.tsx Outdated
Comment thread src/components/ui/Input.tsx Outdated
Comment thread src/components/ui/Input.tsx Outdated
Comment thread src/components/quicklog/QuickLogModal.tsx
Comment thread src/components/ui/Input.tsx Outdated
Comment thread src/components/settings/ProfileForm.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 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/components/quicklog/QuickLogModal.tsx`:
- Around line 80-99: handleSave currently clears selectedTriggers and notes via
setSelectedTriggers([]) and setNotes('') which are async and can allow stale
values to be persisted; fix by capturing the current selectedTriggers and notes
into local constants (e.g., const triggersToSave = selectedTriggers; const
notesToSave = notes.trim() || undefined) and use those locals when calling
addDoseEvent(...) inside handleSave, then clear state (call
setSelectedTriggers([]) and setNotes('')) only after the await addDoseEvent(...)
completes; apply the same change pattern for the other save path referenced (the
block around lines 303-309) so addDoseEvent always receives the captured values,
not potentially-stale state.
- Around line 80-88: handleSave can be called re-entrantly causing duplicate
inserts; add an early return if isSaving is true at the top of the handler.
Inside the handleSave function, check the existing isSaving state (the same
boolean that setIsSaving toggles) and return immediately when true before doing
any validation or state changes so concurrent invocations are ignored.

In `@src/components/ui/Modal.tsx`:
- Around line 19-31: Current useEffect handles Escape and body scroll but lacks
focus trapping and restoration; update the Modal component to trap focus while
open and restore focus to the previously focused element on close. In the
useEffect that runs when isOpen changes (the effect that defines handler, adds
'keydown' listener, and sets document.body.style.overflow), capture
document.activeElement into a variable (e.g., prevFocused) when opening,
programmatically move focus into the modal (to the first focusable element or a
designated element inside the dialog), intercept Tab/Shift+Tab in the 'keydown'
handler to keep focus within the modal (cycle focus between first and last
focusable elements), and on cleanup (when closing) remove the listener, restore
document.body.style.overflow to prevOverflow, and call prevFocused?.focus() to
restore focus to the trigger; use the Modal's container ref to locate focusable
elements.

In `@src/lib/doseHelpers.ts`:
- Around line 39-41: The equality check using Math.abs(minutes - scheduled)
doesn't account for midnight wrap-around; compute the minimal circular
difference between minutes and scheduled (e.g., let raw = Math.abs(minutes -
scheduled); let delta = Math.min(raw, 1440 - raw)) and replace the condition
with if (delta <= TOLERANCE_MIN) return true; reference the existing variables
scheduled, minutes and TOLERANCE_MIN so the function consistently matches
planned doses across midnight.
- Around line 53-62: todayTimeToIso currently only checks for NaN and relies on
Date.setHours which normalizes out-of-range values; update todayTimeToIso to
validate that the input splits into two parts and that hour is an integer
between 0 and 23 and minute is an integer between 0 and 59 (after
parsing/trimming) and throw the existing error message ('שעה לא תקינה') if any
check fails, only then call d.setHours(h, m, 0, 0) and return d.toISOString();
reference the todayTimeToIso function and the h/m variables when making the
changes.
🪄 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 Plus

Run ID: 099776db-2c6d-4273-9de8-9f162b9aa5ec

📥 Commits

Reviewing files that changed from the base of the PR and between 0a47931 and 547cf4b.

📒 Files selected for processing (4)
  • src/components/layout/AppLayout.tsx
  • src/components/quicklog/QuickLogModal.tsx
  • src/components/ui/Modal.tsx
  • src/lib/doseHelpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/layout/AppLayout.tsx

Comment thread src/components/quicklog/QuickLogModal.tsx Outdated
Comment thread src/components/quicklog/QuickLogModal.tsx Outdated
Comment thread src/components/ui/Modal.tsx
Comment thread src/lib/doseHelpers.ts
Comment thread src/lib/doseHelpers.ts
מימוש סעיף 4.1 באפיון. הדשבורד מציג עכשיו את הנתונים שנשמרו
מ-Quick Log, ולא טקסט סטטי:

- TodayCard: סכום מ"ג נוכחי / יעד, מד התקדמות שצבעו משתנה
  לפי המצב (ירוק/צהוב/כתום — בלי אדום), שעת מנה אחרונה,
  באדג' סטטוס.
- StreakCard: רצף ימים ביעד עם חישוב שלא קוטע ביום שעוד לא
  התחיל בו תיעוד, אייקון מתחלף (Sprout → Flame ב-5+ ימים)
  וטקסט מעודד בלי לחץ.
- WeeklyChart: בר-צ'ארט של 7 ימים עם Recharts, קו אופקי =
  יעד יומי, ציר X הפוך לכיוון קריאה עברית, ימים בעברית
  (א-ש), צבע עמודה לפי היחס ליעד. לחיצה מובילה לתובנות.
- InsightCard: תובנת היום מבוססת נתונים — מזהה את יום השבוע
  השכיח של חריגות בחודש האחרון. דורש 5+ ימים של נתונים
  ולפחות 2 חריגות באותו יום כדי לקרוא לזה דפוס, אחרת מציג
  הודעה מעודדת.

תוספות תומכות ב-doseHelpers:
- classifyDayStatus + statusColors: מיפוי בלי אדום.
- localDateKey + aggregateByDay: קיבוץ אירועים ליום
  לפי שעון מקומי (לא UTC) למניעת bug של "חצי לילה זז יום".
- calculateStreak: חישוב רצף עם הגנה מ-365 ימים אחורה.
- buildWeeklySeries: 7 נקודות לגרף.
- getRecentDays: שאילתת טווח של N ימים אחרונים.

הגנת NaN בכל פונקציות הסיכום (כלל 4 ב-CLAUDE.md).
typecheck + build נקיים, dev server מחזיר 200.

https://claude.ai/code/session_01FjJS1YsCyHPNyfQz6nuZBk
Comment thread src/components/dashboard/InsightCard.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/lib/doseHelpers.ts (2)

53-62: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate HH:MM bounds before setHours.

On Lines 57-61, out-of-range values are accepted and normalized by Date, which can shift the saved day/time unexpectedly.

Suggested fix
 export function todayTimeToIso(timeHHMM: string): string {
-  const [hStr, mStr] = timeHHMM.split(':');
-  const h = Number(hStr);
-  const m = Number(mStr);
-  if (Number.isNaN(h) || Number.isNaN(m)) {
+  const [hStr, mStr, extra] = timeHHMM.split(':');
+  const h = Number(hStr?.trim());
+  const m = Number(mStr?.trim());
+  const valid =
+    extra === undefined &&
+    Number.isInteger(h) &&
+    Number.isInteger(m) &&
+    h >= 0 &&
+    h <= 23 &&
+    m >= 0 &&
+    m <= 59;
+  if (!valid) {
     throw new Error('שעה לא תקינה');
   }
   const d = new Date();
   d.setHours(h, m, 0, 0);
   return d.toISOString();
 }
🤖 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/lib/doseHelpers.ts` around lines 53 - 62, The function todayTimeToIso
accepts timeHHMM and parses hStr/mStr into h and m but doesn't validate bounds,
so Date.setHours will normalize out-of-range values and shift the day; update
todayTimeToIso to explicitly validate that h is an integer between 0 and 23 and
m between 0 and 59 (after parsing hStr and mStr), throwing the existing error
('שעה לא תקינה') for invalid values, and only then call d.setHours(h, m, 0, 0)
and return d.toISOString().

39-41: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle midnight wrap-around in planned-dose matching.

The check on Line 40 uses linear distance, so near-midnight planned doses can be missed (e.g., 23:50 vs 00:10).

Suggested fix
     const scheduled = h * 60 + m;
-    if (Math.abs(minutes - scheduled) <= TOLERANCE_MIN) return true;
+    const diff = Math.abs(minutes - scheduled);
+    const circularDiff = Math.min(diff, 1440 - diff);
+    if (circularDiff <= TOLERANCE_MIN) 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/lib/doseHelpers.ts` around lines 39 - 41, The current equality check uses
linear minute distance and misses matches across midnight; replace the simple
Math.abs(minutes - scheduled) comparison with a wrap-aware delta: compute raw =
Math.abs(minutes - scheduled) and delta = Math.min(raw, 1440 - raw), then
compare delta <= TOLERANCE_MIN (use the existing variables scheduled, minutes,
and TOLERANCE_MIN). Update the conditional in the block containing scheduled to
use this wrap-aware delta so planned doses like 23:50 vs 00:10 match correctly.
🤖 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/components/dashboard/InsightCard.tsx`:
- Around line 39-41: The weekday calculation uses new Date(key) which parses
"YYYY-MM-DD" as UTC and can yield the wrong local weekday; instead parse the key
into year/month/day and construct a local Date via new Date(year, monthIndex,
day) before calling getDay(). Replace the line creating d with code that splits
key (e.g., const [y, m, dStr] = key.split('-')), then const d = new
Date(Number(y), Number(m) - 1, Number(dStr)), and keep the rest (dow calculation
and overByWeekday update) unchanged.

In `@src/components/dashboard/StreakCard.tsx`:
- Around line 16-18: The streak calculation is artificially capped by calling
getRecentDays(60) so replace that limited fetch with a full-history fetch (e.g.
call getRecentDays() or a function that returns all days) so useLiveQuery(() =>
getRecentDays(), [], []) (or another unbounded/appropriate param) instead of
getRecentDays(60); keep downstream calls aggregateByDay(events) and
calculateStreak(byDay, user.prescribedDailyDose) unchanged so streaks reflect
the true history.

In `@src/components/dashboard/TodayCard.tsx`:
- Around line 92-96: The progressbar uses aria-valuenow={total} which can exceed
aria-valuemax={user.prescribedDailyDose}; clamp aria-valuenow to not exceed
user.prescribedDailyDose (and not drop below 0) before rendering. Update the
TodayCard component to compute a bounded value (e.g., const boundedTotal =
Math.min(Math.max(total, 0), user.prescribedDailyDose)) and use boundedTotal for
aria-valuenow (and optionally for any visual width calculations) so the
determinate progressbar always respects aria-valuemax.

---

Duplicate comments:
In `@src/lib/doseHelpers.ts`:
- Around line 53-62: The function todayTimeToIso accepts timeHHMM and parses
hStr/mStr into h and m but doesn't validate bounds, so Date.setHours will
normalize out-of-range values and shift the day; update todayTimeToIso to
explicitly validate that h is an integer between 0 and 23 and m between 0 and 59
(after parsing hStr and mStr), throwing the existing error ('שעה לא תקינה') for
invalid values, and only then call d.setHours(h, m, 0, 0) and return
d.toISOString().
- Around line 39-41: The current equality check uses linear minute distance and
misses matches across midnight; replace the simple Math.abs(minutes - scheduled)
comparison with a wrap-aware delta: compute raw = Math.abs(minutes - scheduled)
and delta = Math.min(raw, 1440 - raw), then compare delta <= TOLERANCE_MIN (use
the existing variables scheduled, minutes, and TOLERANCE_MIN). Update the
conditional in the block containing scheduled to use this wrap-aware delta so
planned doses like 23:50 vs 00:10 match correctly.
🪄 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 Plus

Run ID: d782e71d-ac8d-4bed-9db3-c06a1ca127df

📥 Commits

Reviewing files that changed from the base of the PR and between 547cf4b and e98d078.

📒 Files selected for processing (7)
  • src/components/dashboard/InsightCard.tsx
  • src/components/dashboard/StreakCard.tsx
  • src/components/dashboard/TodayCard.tsx
  • src/components/dashboard/WeeklyChart.tsx
  • src/db/repositories/doses.ts
  • src/lib/doseHelpers.ts
  • src/pages/DashboardPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/db/repositories/doses.ts

Comment thread src/components/dashboard/InsightCard.tsx Outdated
Comment thread src/components/dashboard/StreakCard.tsx Outdated
Comment thread src/components/dashboard/TodayCard.tsx
claude added 2 commits May 16, 2026 12:21
Blueprint להפצה כ-Static Site:
- buildCommand: npm ci && npm run build
- publish: ./dist
- Node 22 (Vite 5 דורש Node 20+)
- SPA rewrite: /* → /index.html כדי ש-React Router יעבוד
  בנתיבים כמו /onboarding, /settings וכו'
- כותרות אבטחה: X-Frame-Options, CSP מותאם (מאפשר רק
  Google Fonts כי הגופן Heebo נטען משם), HSTS, ועוד
- cache ארוך לקבצי assets/* כי יש להם hash בשם

https://claude.ai/code/session_01FjJS1YsCyHPNyfQz6nuZBk
…zone

תיקונים תקפים מבדיקת הבוט:

- QuickLogModal:
  * "דלג ותעד" שמר ערכי triggers/notes ישנים בגלל setState
    אסינכרוני — handleSave עכשיו מקבל override מפורש
  * הגנה מ-re-entrancy בלחיצה כפולה (early return אם isSaving)
- Input:
  * החלפת Math.random ב-useId — מזהים יציבים בין רנדורים,
    שומר על קישור label↔input ועל focus
  * aria-describedby מציין רק IDs של אלמנטים שמרונדרים בפועל
- Modal:
  * שמירת האלמנט שהיה בפוקוס לפני פתיחה, העברת פוקוס לתוך
    הדיאלוג, והחזרה לטריגר בסגירה
- TodayCard:
  * קיפוץ aria-valuenow ל-[0, יעד] כדי לא לחרוג מ-aria-valuemax;
    aria-valuetext מציג את הערך האמיתי
- InsightCard:
  * פירוק key (YYYY-MM-DD) ל-Date מקומי במפורש — מונע הסטה
    ביום בשבוע באזורי זמן שליליים
- StreakCard:
  * הגדלת חלון השאילתה ל-365 ימים, תואם לתקרת calculateStreak
- doseHelpers:
  * isPlannedDose: הפרש מעגלי על מחוג 24 שעות (23:50 ↔ 00:10)
  * todayTimeToIso: ולידציית טווחים מפורשת — setHours בולע
    ערכים פסולים בשקט
- doses.ts:
  * נורמליזציה של timestamp ל-UTC Z בכתיבה — מבטיח שאילתות
    טווח לקסיקוגרפיות תקינות גם אם בעתיד יוזן offset
  * ולידציית timestamp גם ב-updateDoseEvent (סימטרי ל-add)
- users.ts:
  * ולידציה הגנתית של scheduledTimes מול regex של HH:MM
- ProfileStep:
  * setIsSaving(false) ב-finally — בהצלחה ניווט קורה, אבל אם
    מסיבה כלשהי לא — הטופס לא נתקע נעול
- ProfileForm:
  * ניהול setTimeout דרך ref, ביטול בעת unmount או שמירה חוזרת
- .gitignore:
  * הרחבת דפוס .env לכל הוריאנטים עם חריגה ל-.env.example

נדחו ביודעין (מצורף ב-thread):
- Bot טען על UTC/local mismatch בשאילתות — לא תקף, כל
  ה-timestamps עוברים toISOString. הוספתי נורמליזציה הגנתית.
- Bot טען שכפתור "חזרה" עושה submit — Button כבר משתמש
  ב-type="button" כברירת-מחדל (Button.tsx:23).
- Self-host של Google Fonts, README markdownlint, Stylelint
  config, focus trap מלא של Tab/Shift+Tab — מחוץ לסקופ.
- שיתוף ולידציה ProfileStep/ProfileForm — ריפקטור, לא באג.

typecheck + build נקיים, dev server מחזיר 200.

https://claude.ai/code/session_01FjJS1YsCyHPNyfQz6nuZBk
@amirbiron amirbiron merged commit 8b14d08 into main May 16, 2026
2 checks passed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 41a2a3d. Configure here.

export function StreakCard({ user }: Props) {
// 365 ימים — תואם לתקרה הפנימית של calculateStreak כדי שרצף ארוך
// לא ייקטע ע"י חלון השאילתה
const events = useLiveQuery(() => getRecentDays(365), [], []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Streak query window off-by-one clips long streaks

Low Severity

When today has no doses logged, calculateStreak starts from yesterday and can look back up to 365 days total (reaching day −366 from today). But getRecentDays(365) only fetches data from day −364 to today (365 calendar days). On the 365th backward step the date key won't exist in the map, the total defaults to 0, and the streak breaks one day early. Passing 366 instead of 365 would fully cover calculateStreak's ceiling.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 41a2a3d. Configure here.

amirbiron pushed a commit that referenced this pull request May 16, 2026
מימוש סעיף 4.3 באפיון. בגרסה זו הטאב "טריגרים" פעיל ושלושת
האחרים הם placeholder אחיד "בגרסה הבאה":

- InsightsPage: ניווט טאבים נגיש (role=tablist/tab/tabpanel,
  aria-selected). מתחיל ב-"טריגרים".
- TriggersTab: חישובים על 30 הימים האחרונים —
  * Map of triggerId → count על כל ה-DoseEvents בתקופה.
  * סיכום מילולי: הטריגר המוביל ואחוז משכיחות הכוללת
    ("דדליין — 40% מבחירות הטריגר, 12 פעמים").
  * בר-צ'ארט אופקי של 8 טריגרים מובילים (Recharts), צבע ברנד
    מודגש לטריגר #1.
  * סיכום פר קטגוריה: צובר את ה-counts לפי category ומציג
    המלצה אחת לכל אחת מ-3 הקטגוריות העיקריות. ההמלצות מקומיות
    בקובץ — אין קריאה לשירות חיצוני.
  * Empty state ידידותי אם עוד לא נבחרו טריגרים — מסביר איפה
    בודקים בתהליך Quick Log.
- ComingSoonTab: רכיב placeholder אחיד לטאבי "דפוסי זמן",
  "מגמה" ו"מתאמים" — מסבירים מה יהיה שם בגרסה הבאה.

חשוב: כל ההמלצות מסומנות במפורש כ"הצעות לחשיבה, לא תחליף
לייעוץ — שינוי מינון תמיד דרך הרופא" (תואם לעקרון המנחה
בסעיף 1 באפיון).

typecheck + build נקיים, /insights מחזיר 200.

https://claude.ai/code/session_01FjJS1YsCyHPNyfQz6nuZBk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants