Skip to content

Comments

feat/ai#4

Open
omarraf wants to merge 3 commits intomainfrom
feat/ai
Open

feat/ai#4
omarraf wants to merge 3 commits intomainfrom
feat/ai

Conversation

@omarraf
Copy link
Owner

@omarraf omarraf commented Feb 24, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced AI Assistant—a chat-based scheduling assistant for creating, refining, and managing your schedules
    • Added email verification requirement for user accounts with resend capability
    • Integrated AI Assistant navigation across mobile bottom menu and desktop sidebar
  • Documentation

    • Comprehensive mobile support and UX improvement strategies documented for responsive design and enhanced mobile experience

@vercel
Copy link

vercel bot commented Feb 24, 2026

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

Project Deployment Actions Updated (UTC)
daychart Ready Ready Preview, Comment Feb 24, 2026 2:46am
time-tracker Ready Ready Preview, Comment Feb 24, 2026 2:46am
time-tracker-qdcw Ready Ready Preview, Comment Feb 24, 2026 2:46am

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

This PR introduces an AI Assistant feature powered by Claude API that enables users to generate schedules conversationally, adds email verification to the authentication flow, updates navigation for desktop and mobile layouts, and includes comprehensive mobile UX and security documentation alongside rendering enhancements for better label fitting.

Changes

Cohort / File(s) Summary
AI Assistant Feature
src/components/AIAssistant.tsx, src/services/aiService.ts
New AI chat component with message history, markdown rendering, and schedule preview/application flow. Service module handles Claude API integration, response parsing for embedded schedule blocks, and context formatting.
Navigation Routing
src/types/schedule.ts, src/components/Sidebar.tsx, src/components/BottomNav.tsx
Expanded NavRoute type to include 'ai-assistant' route. Added corresponding navigation items to both Sidebar and BottomNav with labels and icons.
Dashboard Integration
src/components/Dashboard.tsx
Integrated AIAssistant component with route-conditional rendering, aiMessages state for chat persistence, and onApplySchedule handler to update timeBlocks.
Authentication & Email Verification
src/auth.ts, src/components/AuthButtons.tsx
Added async email verification to signUp flow and new resendVerificationEmail function. AuthButtons now displays verification status banner with resend and reload options.
Component Rendering Enhancements
src/components/CircularChart.tsx, src/components/Timeline.tsx
CircularChart: Arc-aware label fitting with dynamic fontSize, truncation, and text rotation logic. Timeline: Height-based variant rendering (very short, short, medium, long) with adjusted minHeight and compact layouts.
Documentation
.env.example, MOBILE_SUPPORT.md, MOBILE_UX_IMPROVEMENTS.md, SECURITY_TODOS.md
Added Claude API key environment variable. Three new documentation files covering mobile-first design strategy, specific mobile UX fixes with component-level implementation details, and structured security/performance TODOs with priorities and acceptance criteria.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AIAssistant as AIAssistant Component
    participant Dashboard
    participant aiService as AI Service
    participant ClaudeAPI as Claude API
    participant Firebase as Firebase Auth

    User->>AIAssistant: Send message
    AIAssistant->>AIAssistant: Build chat history + context
    AIAssistant->>aiService: sendMessage(messages, timeBlocks)
    aiService->>aiService: Validate API key
    aiService->>aiService: Format schedule context
    aiService->>ClaudeAPI: POST with system prompt & messages
    ClaudeAPI-->>aiService: Response with optional schedule JSON
    aiService->>aiService: Parse schedule blocks & strip JSON
    aiService-->>AIAssistant: AIScheduleResponse (message + timeBlocks)
    AIAssistant->>AIAssistant: Render response & schedule preview
    alt User applies schedule
        User->>AIAssistant: Click "Apply Schedule"
        AIAssistant->>AIAssistant: Confirm replacement
        AIAssistant->>Dashboard: onApplySchedule(newBlocks)
        Dashboard->>Dashboard: Update timeBlocks state
        Dashboard->>Firebase: Save schedule (if authenticated)
        AIAssistant->>AIAssistant: Append confirmation message
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • mobile ux #3: Modifies BottomNav and Dashboard components for mobile UX with route integration; this PR extends those same files to add AI Assistant navigation and dashboard integration, creating a direct code-level dependency.

Poem

🐰 Whiskers twitch with AI delight,
A schedule made by chat so bright,
Claude helps blocks flow, neat and right,
While verification seals the night,
Mobile screens dance in golden light!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat/ai' is overly vague and does not clearly describe the main changes. While it indicates an AI feature, it fails to communicate what specifically is being added or changed. Use a more descriptive title such as 'Add AI Assistant scheduling feature with Claude integration' or 'Implement AI Assistant with Claude API for schedule generation'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ai

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 and usage tips.

Copy link

@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: 11

🧹 Nitpick comments (2)
src/components/AuthButtons.tsx (2)

183-184: Move useState hooks to the top of the component.

isResendingVerification and verificationMessage state are declared on lines 183–184, after the GoogleIcon inner component definition on lines 174–181. All hooks should be declared at the top of the component body before any inner component definitions to maintain conventional hook ordering and readability.

♻️ Suggested refactor
 // At the top of AuthButtons(), with the other useState declarations (lines 12-19):
+  const [isResendingVerification, setIsResendingVerification] = useState(false);
+  const [verificationMessage, setVerificationMessage] = useState('');
 
 // ...
 
   const GoogleIcon = () => ( ... );
 
-  const [isResendingVerification, setIsResendingVerification] = useState(false);
-  const [verificationMessage, setVerificationMessage] = useState('');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AuthButtons.tsx` around lines 183 - 184, Move the two useState
declarations (isResendingVerification and verificationMessage) to the top of the
AuthButtons component body, before any inner component/constant definitions
(e.g., the GoogleIcon inner component), so all hooks are declared in hook order
at the start of the component; locate the lines where useState is called for
isResendingVerification and verificationMessage and cut/paste them above the
GoogleIcon definition (and any other inner components) to ensure hooks run
consistently.

186-200: setTimeout callbacks in handleResendVerification are not cleaned up.

Both setTimeout(() => setVerificationMessage(''), 5000) calls (lines 193 and 196) hold closures over the setter. If the component unmounts before 5 s elapses, these fire against a stale closure. React 18+/19 suppresses the resulting no-op, but it is cleaner to track the timer IDs and clear them (e.g., in a useEffect cleanup or a useRef).

This is low-risk in practice but worth addressing for correctness hygiene.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AuthButtons.tsx` around lines 186 - 200, The two setTimeout
calls in handleResendVerification create timers that can run after unmount; fix
by storing their IDs in a ref (e.g., resendTimerRef) and clearing any existing
timer before starting a new one, use the ref to clearTimeout in a useEffect
cleanup to avoid stale-setState, and update handleResendVerification to set the
ref when scheduling the clearing of setVerificationMessage; keep existing state
setters (setIsResendingVerification, setVerificationMessage) and the resend call
(resendVerificationEmail) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 12-14: The Claude API key is currently exposed as
VITE_CLAUDE_API_KEY and used directly from the client (see
src/services/aiService.ts line ~119); move the key to a server-only env var
(e.g., CLAUDE_API_KEY in .env) and stop using VITE_CLAUDE_API_KEY. Implement a
server-side endpoint (e.g., POST /api/claude or a serverless function handler
such as handleClaudeProxy) that reads process.env.CLAUDE_API_KEY and forwards
client requests to Claude, then update the client code in aiService.ts to call
that endpoint instead of calling Claude directly or reading VITE_CLAUDE_API_KEY;
ensure server-side validation/logging and remove the client-side env reference
from .env.example.

In `@MOBILE_SUPPORT.md`:
- Around line 48-56: The fenced code block(s) in MOBILE_SUPPORT.md are missing
language identifiers (MD040); update each triple-backtick block shown (the block
containing the bullet list "Calculate chart size based on viewport width" etc.)
and any other fenced blocks in the file by adding an appropriate language tag
(e.g., ```text, ```css, ```html or ```typescript) after the opening backticks so
markdownlint passes and code blocks render with consistent language metadata.

In `@MOBILE_UX_IMPROVEMENTS.md`:
- Around line 41-43: The fenced code block containing "[ Save Schedule ] [
Delete All ]" is missing a language identifier (MD040); update the opening fence
from ``` to ```text so the snippet is explicitly marked as plain text,
preserving the content and preventing the MD040 warning.

In `@SECURITY_TODOS.md`:
- Around line 10-27: Consolidate and tighten the Firestore rules for schedules:
remove the duplicate allow create rules and replace them with a single allow
create that validates request.auth != null, request.resource.data.userId ==
request.auth.uid, and enforces the scheduleCount limit via
get(...users/$(request.auth.uid)).data.scheduleCount < 10 (use
request.resource.data.* because resource is undefined on create); keep read as
allow read: if request.auth != null && resource.data.userId == request.auth.uid;
and split write into allow update, delete: if request.auth != null &&
resource.data.userId == request.auth.uid && request.resource.data.userId ==
request.auth.uid to prevent changing userId on updates.

In `@src/auth.ts`:
- Around line 4-9: The signUp function currently allows sendEmailVerification
failures to bubble up and make the whole signup appear to fail; change signUp
(the async function using createUserWithEmailAndPassword and
sendEmailVerification) to catch errors from sendEmailVerification only (wrap the
sendEmailVerification(userCredential.user) call in a try/catch), log or report
the verification-send error (but do not rethrow), and always return the
successfully created userCredential so callers (e.g., AuthButtons.tsx) see the
signup as succeeded even if the verification email fails to send.

In `@src/components/AIAssistant.tsx`:
- Around line 104-195: The chatHistory built in handleSend currently maps all
messages (DisplayMessage) including locally injected assistant messages like
confirmation/error notices; tag local/system messages (e.g., add a boolean flag
like isLocal on DisplayMessage or use a special role value) and update
handleSend to filter out those local/system entries before mapping to
ChatMessage sent to sendMessage, ensuring only user/assistant model-originated
content is included (filtering by the tag or by role) and keep the existing
skip-greeting logic.

In `@src/components/AuthButtons.tsx`:
- Around line 202-207: handleReloadUser currently calls user?.reload() without a
.catch() and then sets the same user object (auth.currentUser) so React won't
re-render; update handleReloadUser to attach a .catch() that logs or surfaces
errors and, on success, increment a lightweight local state "reloadVersion"
(useState number) or toggle a boolean to force a re-render instead of calling
setUser with the same object; keep the existing setUser(auth.currentUser) if you
want but ensure you also bump reloadVersion so the UI (e.g., amber banner)
updates, and reference user.reload(), auth.currentUser, setUser and the new
reloadVersion state in the fix.

In `@src/components/Dashboard.tsx`:
- Around line 641-649: The AI-assistant callback currently applies blocks
directly; instead, in the onApplySchedule passed to AIAssistant validate each
incoming block with validateTimeBlock, drop or collect invalid ones, then pass
the filtered list through sortTimeBlocks before calling
setTimeBlocks(newBlocksSorted); reference the AIAssistant prop onApplySchedule,
the validateTimeBlock function, sortTimeBlocks helper, and the setTimeBlocks
state setter when making this change so AI-generated blocks follow the same
validation/sorting rules as manual blocks.

In `@src/components/Timeline.tsx`:
- Around line 225-240: The minHeight of 20px makes touch targets too small and
the text-fitting booleans (pixelHeight, isVeryShort, isShort, isMedium) still
use raw duration-based pixelHeight, so short blocks get compact layouts even
when minHeight inflates their visual size; change the block render to use a 44px
minimum touch target (style minHeight: '44px') and adjust the sizing logic:
compute a renderedPixelHeight = Math.max(pixelHeight, 44) (or convert minHeight
to the same coordinate system) and base isVeryShort/isShort/isMedium on
renderedPixelHeight instead of pixelHeight so label/layout decisions reflect the
actual rendered height on touch devices while keeping the minHeight enforced on
the div.

In `@src/services/aiService.ts`:
- Around line 45-60: parseScheduleFromResponse currently trusts the AI JSON and
creates TimeBlock IDs immediately; add a validation/sanitization pass before
mapping to TimeBlock: parse the JSON as now, then filter into a sanitized array
(e.g., sanitized) that only keeps objects with required properties (startTime,
endTime, label) where startTime/endTime match /^(\d{2}):(\d{2})$/ and hours
0–23, minutes 0–59 and minutes % 5 === 0, and optionally ensure startTime <
endTime and no overlapping blocks; if sanitized is empty return null, then map
sanitized to TimeBlock objects (id via crypto.randomUUID(), order = index,
default color '#60a5fa'), leaving parseScheduleFromResponse and TimeBlock shape
intact.
- Around line 3-5: The client currently exposes CLAUDE_API_KEY and calls
Anthropic directly (CLAUDE_API_KEY, CLAUDE_API_URL and the
"anthropic-dangerous-direct-browser-access" header) which must be removed from
frontend; instead move the secret to a backend/serverless proxy that reads the
key from a server env var and forwards requests to CLAUDE_API_URL, then update
the frontend to call that proxy endpoint (no API key or dangerous header sent
from the browser). In practice: delete CLAUDE_API_KEY usage in
src/services/aiService.ts, implement a server handler that uses the env var to
call Anthropic and returns the response, and change client request code to call
the new proxy endpoint.

---

Nitpick comments:
In `@src/components/AuthButtons.tsx`:
- Around line 183-184: Move the two useState declarations
(isResendingVerification and verificationMessage) to the top of the AuthButtons
component body, before any inner component/constant definitions (e.g., the
GoogleIcon inner component), so all hooks are declared in hook order at the
start of the component; locate the lines where useState is called for
isResendingVerification and verificationMessage and cut/paste them above the
GoogleIcon definition (and any other inner components) to ensure hooks run
consistently.
- Around line 186-200: The two setTimeout calls in handleResendVerification
create timers that can run after unmount; fix by storing their IDs in a ref
(e.g., resendTimerRef) and clearing any existing timer before starting a new
one, use the ref to clearTimeout in a useEffect cleanup to avoid stale-setState,
and update handleResendVerification to set the ref when scheduling the clearing
of setVerificationMessage; keep existing state setters
(setIsResendingVerification, setVerificationMessage) and the resend call
(resendVerificationEmail) intact.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fdc571 and 66ad859.

📒 Files selected for processing (14)
  • .env.example
  • MOBILE_SUPPORT.md
  • MOBILE_UX_IMPROVEMENTS.md
  • SECURITY_TODOS.md
  • src/auth.ts
  • src/components/AIAssistant.tsx
  • src/components/AuthButtons.tsx
  • src/components/BottomNav.tsx
  • src/components/CircularChart.tsx
  • src/components/Dashboard.tsx
  • src/components/Sidebar.tsx
  • src/components/Timeline.tsx
  • src/services/aiService.ts
  • src/types/schedule.ts

Comment on lines +12 to +14
# Claude AI Assistant
# Get your API key from: https://console.anthropic.com/
VITE_CLAUDE_API_KEY=example_key
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all Claude key usage to confirm client exposure
rg -n "VITE_CLAUDE_API_KEY|CLAUDE_API_KEY|claude" -S

Repository: omarraf/time-tracker

Length of output: 518


🏁 Script executed:

#!/bin/bash
# Get full context of aiService.ts to see the API call and any proxy setup
cat -n src/services/aiService.ts

Repository: omarraf/time-tracker

Length of output: 6060


Move Claude API key to server-side environment—do not expose in client builds.

VITE_ variables are bundled into the client by Vite, so this key is exposed to anyone inspecting the app. Currently, the API key is used directly from client-side code (src/services/aiService.ts:119) to call the Claude API. Create a server-side endpoint (serverless function, backend route, or proxy) and call Claude through that instead, keeping the API key secure on the server.

🔧 Suggested .env.example change
-# Claude AI Assistant
-# Get your API key from: https://console.anthropic.com/
-VITE_CLAUDE_API_KEY=example_key
+# Claude AI Assistant (server-side only; do not expose in client builds)
+CLAUDE_API_KEY=example_key
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 12 - 14, The Claude API key is currently exposed
as VITE_CLAUDE_API_KEY and used directly from the client (see
src/services/aiService.ts line ~119); move the key to a server-only env var
(e.g., CLAUDE_API_KEY in .env) and stop using VITE_CLAUDE_API_KEY. Implement a
server-side endpoint (e.g., POST /api/claude or a serverless function handler
such as handleClaudeProxy) that reads process.env.CLAUDE_API_KEY and forwards
client requests to Claude, then update the client code in aiService.ts to call
that endpoint instead of calling Claude directly or reading VITE_CLAUDE_API_KEY;
ensure server-side validation/logging and remove the client-side env reference
from .env.example.

Comment on lines +48 to +56
**Changes:**
```
- Calculate chart size based on viewport width
- Add horizontal padding: 16px on mobile, 24px on tablet
- Center chart using flex container
- Prevent overflow with: overflow-x: hidden on parent
- Max width: min(100vw - 32px, 500px) for chart container
- Position absolutely within centered container
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language tags to fenced code blocks (MD040).

Markdownlint reports missing language identifiers. Please add a language (e.g., text, html, css, typescript) to each fenced block for consistency.

🔧 Example fix
-```
+```text
- Calculate chart size based on viewport width
- Add horizontal padding: 16px on mobile, 24px on tablet
- Center chart using flex container
- Prevent overflow with: overflow-x: hidden on parent
- Max width: min(100vw - 32px, 500px) for chart container
- Position absolutely within centered container
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Changes:**
```
- Calculate chart size based on viewport width
- Add horizontal padding: 16px on mobile, 24px on tablet
- Center chart using flex container
- Prevent overflow with: overflow-x: hidden on parent
- Max width: min(100vw - 32px, 500px) for chart container
- Position absolutely within centered container
```
**Changes:**
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MOBILE_SUPPORT.md` around lines 48 - 56, The fenced code block(s) in
MOBILE_SUPPORT.md are missing language identifiers (MD040); update each
triple-backtick block shown (the block containing the bullet list "Calculate
chart size based on viewport width" etc.) and any other fenced blocks in the
file by adding an appropriate language tag (e.g., ```text, ```css, ```html or
```typescript) after the opening backticks so markdownlint passes and code
blocks render with consistent language metadata.

Comment on lines +41 to +43
```
[ Save Schedule ] [ Delete All ]
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Specify a language for the fenced code block.

This block is missing a language identifier, which triggers MD040.

📝 Suggested fix
-```
+```text
 [ Save Schedule ] [ Delete All ]
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 41-41: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MOBILE_UX_IMPROVEMENTS.md` around lines 41 - 43, The fenced code block
containing "[ Save Schedule ] [ Delete All ]" is missing a language identifier
(MD040); update the opening fence from ``` to ```text so the snippet is
explicitly marked as plain text, preserving the content and preventing the MD040
warning.

Comment on lines +10 to +27
```javascript
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /schedules/{scheduleId} {
// Only schedule owner can read/write their own schedules
allow read, write: if request.auth != null
&& request.auth.uid == resource.data.userId;

// Only owner can create schedules
allow create: if request.auth != null
&& request.auth.uid == request.resource.data.userId;

// Prevent creating more than 10 schedules per user
allow create: if request.auth != null
&& request.auth.uid == request.resource.data.userId
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.scheduleCount < 10;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Firestore security rules multiple allow statements evaluation semantics OR

💡 Result:

In Cloud Firestore Security Rules, multiple allow statements that apply to the same request are combined as a logical OR:

  • Within a match, if any allow rule for the requested method is satisfied, the request is allowed. [1]
  • If a document/path matches multiple match blocks, the result is still OR across all matching allow expressions (“allowed if any condition is true”). [2]

For “lazy” evaluation (to avoid unnecessary get()/exists() calls):

  • && and || are short-circuiting in the Rules language, so in an expression like a || b, b is only evaluated if a is false. [1]

Practical takeaway: if you care about guaranteeing short-circuit order for expensive lookups, put the cheap check first in a single condition using || / && (since that order is defined), rather than relying on the order of separate allow statements. [1]

Sources:
[1] Firebase Docs — Security Rules language (Allow; short-circuiting &&/||)
[2] Firebase Docs — How Security Rules work (Overlapping match statements; OR semantics)


🌐 Web query:

Firestore security rules resource.data vs request.resource.data create update

💡 Result:

In Firestore Security Rules:

  • resource.data = the current document as stored in Firestore (the “before” state). It exists for reads, updates, deletes; it’s not available on create because there is no existing document yet. (firebase.google.com)
  • request.resource.data = the document as it would look after the write (the “after” / “pending” state). For update, this is the full merged document (existing fields + changes), not just the fields the client sent. (firebase.google.com)

How to use them by operation

Create

  • You validate the incoming document using request.resource.data (required fields, types, allowed keys, etc.). (firebase.google.com)
  • resource.data is not meaningful on create. (firebase.google.com)

Update

  • Use both:
    • resource.data to check what’s currently stored (e.g., ownership, immutable fields).
    • request.resource.data to check what the document will become.
  • To restrict which fields changed, compare them with diff():
    • request.resource.data.diff(resource.data).affectedKeys() gives the set of fields touched by the update. (firebase.google.com)

Example pattern (owner can update but can’t change author_uid): (firebase.google.com)

allow update: if request.auth.uid == request.resource.data.author_uid
              && request.auth.uid == resource.data.author_uid;

Example field-level update restriction (only allow changing certain fields): (firebase.google.com)

allow update: if request.resource.data.diff(resource.data).affectedKeys()
              .hasOnly(['name','location']);

Citations:


🌐 Web query:

Firestore rules null checks resource.data on create operation

💡 Result:

On a create in Firestore Security Rules, the document doesn’t exist yet, so resource is null. That means any access to resource.data can throw (and the overall expression evaluates to false/denied), even if it looks like a tautology. (stackoverflow.com)

Use these patterns instead:

1) Check “create vs update” safely

allow write: if resource == null;        // create
allow write: if resource != null;        // update

(or inside a combined rule)

allow write: if (resource == null && /* create checks */)
          || (resource != null && /* update checks */);

(stackoverflow.com)

You can also use:

request.method == 'create'

(stackoverflow.com)

2) Validate fields on create: use request.resource.data

request.resource.data is the incoming (“future”) document for writes; it’s what you should check on create. (firebase.google.com)

Example “null check” on create:

allow create: if request.resource.data.optionalField == null;

(code.luasoftware.com)

3) If you must reference existing data, guard it

allow write: if resource == null
          || resource.data.someField == 'x';

(guard with resource == null before touching resource.data). (stackoverflow.com)

Citations:


Consolidate Firestore rules to eliminate bypass of schedule limits and prevent userId changes.

The two allow create rules are evaluated as logical OR; the first allows creation without the scheduleCount check, making the limit bypassable. Additionally, the allow read, write rule uses resource.data.userId which is inaccessible on create (causing the rule to fail), and it lacks protection against updating the userId field. Separate read, create, and update+delete rules with proper field validation are needed.

Consider the proposed rules:

  • read: checks resource.data.userId (safe; resource exists on read)
  • create: single consolidated rule checking request.resource.data.userId and the scheduleCount limit (prevents OR bypass)
  • update, delete: enforces both current ownership via resource.data.userId and prevents userId reassignment via request.resource.data.userId == request.auth.uid
Proposed rules (consolidated)
-      allow read, write: if request.auth != null
-        && request.auth.uid == resource.data.userId;
-
-      allow create: if request.auth != null
-        && request.auth.uid == request.resource.data.userId;
-
-      allow create: if request.auth != null
-        && request.auth.uid == request.resource.data.userId
-        && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.scheduleCount < 10;
+      allow read: if request.auth != null
+        && request.auth.uid == resource.data.userId;
+
+      allow create: if request.auth != null
+        && request.auth.uid == request.resource.data.userId
+        && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.scheduleCount < 10;
+
+      allow update, delete: if request.auth != null
+        && request.auth.uid == resource.data.userId
+        && request.resource.data.userId == request.auth.uid;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SECURITY_TODOS.md` around lines 10 - 27, Consolidate and tighten the
Firestore rules for schedules: remove the duplicate allow create rules and
replace them with a single allow create that validates request.auth != null,
request.resource.data.userId == request.auth.uid, and enforces the scheduleCount
limit via get(...users/$(request.auth.uid)).data.scheduleCount < 10 (use
request.resource.data.* because resource is undefined on create); keep read as
allow read: if request.auth != null && resource.data.userId == request.auth.uid;
and split write into allow update, delete: if request.auth != null &&
resource.data.userId == request.auth.uid && request.resource.data.userId ==
request.auth.uid to prevent changing userId on updates.

Comment on lines +4 to 9
export const signUp = async (email: string, password: string) => {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
// Send email verification immediately after signup
await sendEmailVerification(userCredential.user);
return userCredential;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

sendEmailVerification failure silently invalidates a successfully created account.

createUserWithEmailAndPassword on line 5 already registers the user in Firebase. If sendEmailVerification on line 7 then throws (e.g., auth/too-many-requests, transient network error), the exception propagates out of signUp, and the caller in AuthButtons.tsx catches it and renders an error message. The user sees a failure, retries, and gets auth/email-already-in-use — their account exists but they can't proceed.

Verification email delivery is a non-critical post-registration step. Wrap it so that its failure doesn't surface as a sign-up failure:

🛡️ Proposed fix
 export const signUp = async (email: string, password: string) => {
     const userCredential = await createUserWithEmailAndPassword(auth, email, password);
-    // Send email verification immediately after signup
-    await sendEmailVerification(userCredential.user);
+    // Best-effort: send verification email; failure is non-fatal
+    try {
+        await sendEmailVerification(userCredential.user);
+    } catch {
+        // Account is created; user can resend verification later
+    }
     return userCredential;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const signUp = async (email: string, password: string) => {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
// Send email verification immediately after signup
await sendEmailVerification(userCredential.user);
return userCredential;
};
export const signUp = async (email: string, password: string) => {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
// Best-effort: send verification email; failure is non-fatal
try {
await sendEmailVerification(userCredential.user);
} catch {
// Account is created; user can resend verification later
}
return userCredential;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auth.ts` around lines 4 - 9, The signUp function currently allows
sendEmailVerification failures to bubble up and make the whole signup appear to
fail; change signUp (the async function using createUserWithEmailAndPassword and
sendEmailVerification) to catch errors from sendEmailVerification only (wrap the
sendEmailVerification(userCredential.user) call in a try/catch), log or report
the verification-send error (but do not rethrow), and always return the
successfully created userCredential so callers (e.g., AuthButtons.tsx) see the
signup as succeeded even if the verification email fails to send.

Comment on lines +202 to +207
const handleReloadUser = () => {
user?.reload().then(() => {
// Force a re-render by setting user again
setUser(auth.currentUser);
});
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

handleReloadUser has an unhandled rejection and will not trigger a re-render.

Two problems:

1. Missing .catch(): user?.reload() returns a Promise<void>. Without a .catch(), any rejection (e.g., network failure) becomes an unhandled promise rejection, silently swallowed — the user gets no feedback.

2. Re-render won't fire: Firebase's User.reload() refreshes properties in-place on the same User instance. auth.currentUser after reload returns that same object reference. setUser(auth.currentUser) then calls Object.is(prevUser, nextUser)true → React bails out without re-rendering. The amber banner will never disappear when the user clicks "Already Verified?", which is the sole purpose of this button.

onAuthStateChanged does not fire on reload(), so the subscription in useEffect won't rescue this either.

Fix: introduce a cheap version counter to force a re-render after reload.

🐛 Proposed fix
+  const [userVersion, setUserVersion] = useState(0);
 
   const handleReloadUser = () => {
-    user?.reload().then(() => {
-      // Force a re-render by setting user again
-      setUser(auth.currentUser);
-    });
+    user?.reload().then(() => {
+      setUser(auth.currentUser);
+      setUserVersion(v => v + 1); // force re-render regardless of object identity
+    }).catch(() => {
+      setVerificationMessage('Could not refresh status. Please check your connection.');
+      setTimeout(() => setVerificationMessage(''), 5000);
+    });
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/AuthButtons.tsx` around lines 202 - 207, handleReloadUser
currently calls user?.reload() without a .catch() and then sets the same user
object (auth.currentUser) so React won't re-render; update handleReloadUser to
attach a .catch() that logs or surfaces errors and, on success, increment a
lightweight local state "reloadVersion" (useState number) or toggle a boolean to
force a re-render instead of calling setUser with the same object; keep the
existing setUser(auth.currentUser) if you want but ensure you also bump
reloadVersion so the UI (e.g., amber banner) updates, and reference
user.reload(), auth.currentUser, setUser and the new reloadVersion state in the
fix.

Comment on lines +641 to +649
{currentRoute === 'ai-assistant' && (
<AIAssistant
timeBlocks={timeBlocks}
messages={aiMessages}
setMessages={setAiMessages}
onApplySchedule={(newBlocks) => {
setTimeBlocks(newBlocks);
}}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect AI assistant schedule generation and validation
rg -n "onApplySchedule|timeBlocks|validateTimeBlock|sortTimeBlocks" -S src/components/AIAssistant.tsx src/utils/timeUtils.ts

Repository: omarraf/time-tracker

Length of output: 1252


🏁 Script executed:

#!/bin/bash
# Check handleApplySchedule implementation and surrounding context
sed -n '170,200p' src/components/AIAssistant.tsx

Repository: omarraf/time-tracker

Length of output: 905


🏁 Script executed:

#!/bin/bash
# Find and inspect sendMessage function
rg -n "const sendMessage|function sendMessage" src/components/AIAssistant.tsx -A 30

Repository: omarraf/time-tracker

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check imports and sendMessage function in AIAssistant.tsx
head -50 src/components/AIAssistant.tsx | grep -n "import\|sendMessage"
# Also search for sendMessage anywhere in the file
rg -n "sendMessage" src/components/AIAssistant.tsx -B 2 -A 5

Repository: omarraf/time-tracker

Length of output: 1035


🏁 Script executed:

#!/bin/bash
# Check aiService implementation
cat src/services/aiService.ts

Repository: omarraf/time-tracker

Length of output: 4975


🏁 Script executed:

#!/bin/bash
# Find manual block creation/validation in Dashboard.tsx
rg -n "addTimeBlock|validateTimeBlock|setTimeBlocks" src/components/Dashboard.tsx -B 3 -A 8 | head -80

Repository: omarraf/time-tracker

Length of output: 2876


🏁 Script executed:

#!/bin/bash
# Find onApplySchedule handler in Dashboard.tsx
rg -n "onApplySchedule" src/components/Dashboard.tsx -B 2 -A 5

Repository: omarraf/time-tracker

Length of output: 295


Add validation and sorting to AI-generated blocks before applying to state.

onApplySchedule applies blocks directly without validation, unlike manual block creation which calls validateTimeBlock() and sortTimeBlocks(). If the AI returns overlapping or unsorted blocks, they bypass the validation enforced for manual blocks, potentially breaking downstream rendering and state expectations.

🛡️ Suggested in-place validation
-            onApplySchedule={(newBlocks) => {
-              setTimeBlocks(newBlocks);
-            }}
+            onApplySchedule={(newBlocks) => {
+              const sorted = sortTimeBlocks(newBlocks);
+              const validated: TimeBlock[] = [];
+
+              for (const block of sorted) {
+                const { valid, message } = validateTimeBlock(
+                  { startTime: block.startTime, endTime: block.endTime },
+                  validated
+                );
+                if (!valid) {
+                  alert(message);
+                  return;
+                }
+                validated.push(block);
+              }
+
+              setTimeBlocks(validated);
+            }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{currentRoute === 'ai-assistant' && (
<AIAssistant
timeBlocks={timeBlocks}
messages={aiMessages}
setMessages={setAiMessages}
onApplySchedule={(newBlocks) => {
setTimeBlocks(newBlocks);
}}
/>
{currentRoute === 'ai-assistant' && (
<AIAssistant
timeBlocks={timeBlocks}
messages={aiMessages}
setMessages={setAiMessages}
onApplySchedule={(newBlocks) => {
const sorted = sortTimeBlocks(newBlocks);
const validated: TimeBlock[] = [];
for (const block of sorted) {
const { valid, message } = validateTimeBlock(
{ startTime: block.startTime, endTime: block.endTime },
validated
);
if (!valid) {
alert(message);
return;
}
validated.push(block);
}
setTimeBlocks(validated);
}}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Dashboard.tsx` around lines 641 - 649, The AI-assistant
callback currently applies blocks directly; instead, in the onApplySchedule
passed to AIAssistant validate each incoming block with validateTimeBlock, drop
or collect invalid ones, then pass the filtered list through sortTimeBlocks
before calling setTimeBlocks(newBlocksSorted); reference the AIAssistant prop
onApplySchedule, the validateTimeBlock function, sortTimeBlocks helper, and the
setTimeBlocks state setter when making this change so AI-generated blocks follow
the same validation/sorting rules as manual blocks.

Comment on lines +225 to 240
// Calculate pixel height for smart text fitting
const pixelHeight = (segment.duration / totalDayMinutes) * timelineHeight;
const isVeryShort = pixelHeight < 28;
const isShort = pixelHeight < 44;
const isMedium = pixelHeight < 60;

return (
<div
key={`${block.id}-${index}`}
className="absolute left-12 sm:left-14 right-2 sm:right-4 rounded-lg cursor-pointer transition-all hover:shadow-lg active:scale-95 sm:hover:scale-[1.02] group touch-manipulation"
className="absolute left-12 sm:left-14 right-2 sm:right-4 rounded-lg cursor-pointer transition-all hover:shadow-lg active:scale-95 sm:hover:scale-[1.02] group touch-manipulation overflow-hidden"
style={{
top: `${top}%`,
height: `${height}%`,
backgroundColor: block.color,
minHeight: '30px',
minHeight: '20px',
}}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Touch targets may be too small on mobile; align minHeight with label fitting.

Reducing minHeight to 20px can make short blocks difficult to tap on touch devices and conflicts with the 44px touch target guidance in MOBILE_SUPPORT.md. Also, label fitting uses pixelHeight derived from duration, so if minHeight inflates the rendered size, the UI still chooses overly-compact layouts.

💡 Suggested adjustment
-        const pixelHeight = (segment.duration / totalDayMinutes) * timelineHeight;
-        const isVeryShort = pixelHeight < 28;
+        const rawPixelHeight = (segment.duration / totalDayMinutes) * timelineHeight;
+        const minPixelHeight = isMobile ? 44 : 20;
+        const pixelHeight = Math.max(rawPixelHeight, minPixelHeight);
+        const isVeryShort = pixelHeight < 28;
...
-              minHeight: '20px',
+              minHeight: `${minPixelHeight}px`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Timeline.tsx` around lines 225 - 240, The minHeight of 20px
makes touch targets too small and the text-fitting booleans (pixelHeight,
isVeryShort, isShort, isMedium) still use raw duration-based pixelHeight, so
short blocks get compact layouts even when minHeight inflates their visual size;
change the block render to use a 44px minimum touch target (style minHeight:
'44px') and adjust the sizing logic: compute a renderedPixelHeight =
Math.max(pixelHeight, 44) (or convert minHeight to the same coordinate system)
and base isVeryShort/isShort/isMedium on renderedPixelHeight instead of
pixelHeight so label/layout decisions reflect the actual rendered height on
touch devices while keeping the minHeight enforced on the div.

Comment on lines +3 to +5
const CLAUDE_API_KEY = import.meta.env.VITE_CLAUDE_API_KEY || 'your-claude-api-key-here';
const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "aiService.ts" -type f

Repository: omarraf/time-tracker

Length of output: 91


🏁 Script executed:

cat -n src/services/aiService.ts | head -130

Repository: omarraf/time-tracker

Length of output: 5079


🌐 Web query:

Anthropic Messages API security client-side browser requests anthropic-dangerous-direct-browser-access

💡 Result:

Anthropic’s Messages API can be called directly from a web browser (CORS), but Anthropic makes this an explicit “opt-in” because it can easily lead to API key exposure.

  • CORS / browser access: Anthropic added browser SDK support by returning CORS headers (Aug 22, 2024 release note). To use it in the official TypeScript SDK you must enable dangerouslyAllowBrowser: true. [1]
  • What anthropic-dangerous-direct-browser-access is: For direct fetch() calls from a browser to https://api.anthropic.com/v1/messages, you must include the request header anthropic-dangerous-direct-browser-access: true (otherwise you may get an auth/CORS-style error telling you to set that header). [2][3]
  • Security model (why “dangerous”): Enabling browser usage is labeled dangerous because it puts your secret API credentials in client-side code, where users (or malicious scripts/extensions) can inspect/extract/misuse them. Anthropic’s SDK docs explicitly warn that browsers are less secure and this can lead to unauthorized usage. [4]

Practical guidance

  • For public apps: use a server-side proxy (keep the API key on your backend).
  • If you must do client-side: only consider it for trusted/internal tools or BYO-key apps (users provide their own key), and treat the key as compromised-by-design (rotate/revoke aggressively). [2][4]

Sources:
[1] Anthropic API release notes (Aug 22, 2024 CORS/browser SDK support) (docs.anthropic.com)
[2] Simon Willison write-up documenting the required anthropic-dangerous-direct-browser-access: true header (simonwillison.net)
[3] Example error message: “CORS requests must set 'anthropic-dangerous-direct-browser-access' header” (github.com)
[4] Official Anthropic TypeScript SDK warning about dangerouslyAllowBrowser exposing secret credentials (github.com)

Citations:


Move Claude API key to a backend/serverless proxy—do not ship it to the browser.

The VITE_CLAUDE_API_KEY is bundled into the client and sent directly to Anthropic's API using the anthropic-dangerous-direct-browser-access header. This exposes the key to every user, allowing them to make unauthorized API calls and incur costs on your account.

Anthropic explicitly labels this pattern as "dangerous" in their documentation because client-side API credentials are inherently compromised. Keep the API key on your backend and proxy requests through a server endpoint instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/aiService.ts` around lines 3 - 5, The client currently exposes
CLAUDE_API_KEY and calls Anthropic directly (CLAUDE_API_KEY, CLAUDE_API_URL and
the "anthropic-dangerous-direct-browser-access" header) which must be removed
from frontend; instead move the secret to a backend/serverless proxy that reads
the key from a server env var and forwards requests to CLAUDE_API_URL, then
update the frontend to call that proxy endpoint (no API key or dangerous header
sent from the browser). In practice: delete CLAUDE_API_KEY usage in
src/services/aiService.ts, implement a server handler that uses the env var to
call Anthropic and returns the response, and change client request code to call
the new proxy endpoint.

Comment on lines +45 to +60
export const parseScheduleFromResponse = (text: string): TimeBlock[] | null => {
const scheduleMatch = text.match(/```schedule\s*\n([\s\S]*?)\n```/);
if (!scheduleMatch) return null;

try {
const parsed = JSON.parse(scheduleMatch[1]);
if (!Array.isArray(parsed)) return null;

return parsed.map((block: { startTime: string; endTime: string; label: string; color: string }, index: number) => ({
id: crypto.randomUUID(),
startTime: block.startTime,
endTime: block.endTime,
label: block.label,
color: block.color || '#60a5fa',
order: index,
}));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate AI-produced time blocks before creating IDs.

parseScheduleFromResponse trusts AI output; invalid time formats, non‑5‑minute increments, or overlaps can propagate corrupted data into the scheduler. Add input validation and discard (or normalize) invalid blocks.

🛡️ Suggested validation pass
 export const parseScheduleFromResponse = (text: string): TimeBlock[] | null => {
   const scheduleMatch = text.match(/```schedule\s*\n([\s\S]*?)\n```/);
   if (!scheduleMatch) return null;

   try {
     const parsed = JSON.parse(scheduleMatch[1]);
     if (!Array.isArray(parsed)) return null;
+    const isValidTime = (t: string) => {
+      const m = /^(\d{2}):(\d{2})$/.exec(t);
+      if (!m) return false;
+      const hh = Number(m[1]);
+      const mm = Number(m[2]);
+      return hh >= 0 && hh <= 23 && mm >= 0 && mm < 60 && mm % 5 === 0;
+    };
+    const sanitized = parsed.filter((b: any) =>
+      b && isValidTime(b.startTime) && isValidTime(b.endTime)
+    );
+    if (sanitized.length === 0) return null;

-    return parsed.map((block: { startTime: string; endTime: string; label: string; color: string }, index: number) => ({
+    return sanitized.map((block: { startTime: string; endTime: string; label: string; color: string }, index: number) => ({
       id: crypto.randomUUID(),
       startTime: block.startTime,
       endTime: block.endTime,
       label: block.label,
       color: block.color || '#60a5fa',
       order: index,
     }));
   } catch {
     return null;
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/aiService.ts` around lines 45 - 60, parseScheduleFromResponse
currently trusts the AI JSON and creates TimeBlock IDs immediately; add a
validation/sanitization pass before mapping to TimeBlock: parse the JSON as now,
then filter into a sanitized array (e.g., sanitized) that only keeps objects
with required properties (startTime, endTime, label) where startTime/endTime
match /^(\d{2}):(\d{2})$/ and hours 0–23, minutes 0–59 and minutes % 5 === 0,
and optionally ensure startTime < endTime and no overlapping blocks; if
sanitized is empty return null, then map sanitized to TimeBlock objects (id via
crypto.randomUUID(), order = index, default color '#60a5fa'), leaving
parseScheduleFromResponse and TimeBlock shape intact.

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