Skip to content

feat/ai#4

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

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

Conversation

@omarraf
Copy link
Owner

@omarraf omarraf commented Feb 24, 2026

Summary by CodeRabbit

  • New Features

    • AI Assistant chat to generate and apply schedules, plus new AI navigation entry
    • Backend AI endpoints and usage/rate-limit handling for authenticated users
    • Email verification flow with resend and reload status
  • UI Improvements

    • Smarter chart and timeline label fitting; schedule list panel; mobile-first navigation and header/footer adjustments
  • Documentation

    • Mobile UX & support plan and security TODOs
  • Config

    • New API URL env var and cloud functions deployment settings

@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 27, 2026 10:08pm
time-tracker Ready Ready Preview, Comment Feb 27, 2026 10:08pm
time-tracker-qdcw Ready Ready Preview, Comment Feb 27, 2026 10:08pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 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

Adds an AI Assistant feature (client chat UI, client aiService, Express backend, and Firebase function using Anthropic), navigation and type updates, email verification/resend flows, chart/timeline rendering improvements, mobile UX & security docs, and server/functions scaffolding with env/config updates.

Changes

Cohort / File(s) Summary
AI Assistant client components
src/components/AIAssistant.tsx, src/components/ScheduleListPanel.tsx, src/components/Dashboard.tsx
New AIAssistant chat UI and ScheduleListPanel modal; Dashboard wired to render AI assistant, persist aiMessages, and apply AI-provided schedules.
AI client service
src/services/aiService.ts
New aiService with sendMessage and getUsage, uses VITE_API_URL env var, authenticates via Firebase token, handles 401/429, enriches returned timeBlocks.
Backend & cloud function
server/src/index.ts, functions/src/index.ts, server/..., functions/...
Adds Express backend and Firebase Function calling Anthropic Claude, parsing optional JSON schedule blocks, returning message + optional timeBlocks; includes usage tracking and rate-limiting.
Auth & Firebase client
src/auth.ts, src/firebase.ts, src/components/AuthButtons.tsx
signUp made async and sends verification email; new resendVerificationEmail; Auth UI shows verification banner with resend/reload flows; firebase client now exports functions.
Navigation & types
src/types/schedule.ts, src/components/Sidebar.tsx, src/components/BottomNav.tsx
Adds ai-assistant nav route and inserts corresponding Sidebar and BottomNav items/icons.
Chart & timeline rendering
src/components/CircularChart.tsx, src/components/Timeline.tsx
CircularChart: arc-aware label fitting, truncation and rotation; Timeline: pixel-height–based rendering variants, reduced minHeights, overflow handling.
Mobile UX & docs
MOBILE_SUPPORT.md, MOBILE_UX_IMPROVEMENTS.md
Adds comprehensive mobile-first plans, phased rollout steps, component-specific guidance, and testing criteria.
Security / ops docs
SECURITY_TODOS.md
New prioritized security and performance TODOs with implementation notes, checks, and monitoring suggestions.
Server/functions scaffolding & config
server/package.json, functions/package.json, *.tsconfig.json, firebase.json
Adds Node/TypeScript project configs, functions runtime set to nodejs20, and build/dev scripts for server and functions.
Env, gitignore & misc
.env.example, .gitignore
Adds VITE_API_URL to env example; ignores functions/lib and server/dist.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as AIAssistant Component
    participant Client as aiService (client)
    participant Firebase as Firebase Auth
    participant Server as Backend (Express)
    participant Function as Firebase Function
    participant Claude as Anthropic Claude

    User->>UI: types message & send
    UI->>Client: sendMessage(messages, timeBlocks)
    Client->>Firebase: getAuthToken()
    Client->>Server: POST /api/ai/message (Authorization: Bearer)
    alt Server proxies to cloud function
        Server->>Function: invoke sendAIMessage
        Function->>Claude: request with system prompt + context
        Claude-->>Function: response (may include JSON schedule)
        Function-->>Server: { message, timeBlocks? }
    else Server calls Claude directly
        Server->>Claude: request with system prompt + context
        Claude-->>Server: response (may include JSON schedule)
    end
    Server->>Server: parse schedule JSON, strip block from message
    Server-->>Client: { message, timeBlocks? }
    Client-->>UI: AIScheduleResponse
    UI->>UI: render assistant message and schedule preview
    alt user applies schedule
        UI->>Dashboard: onApplySchedule(newBlocks)
        Dashboard->>Firebase: save schedule (if authenticated)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • mobile ux #3: Overlapping edits to BottomNav, CircularChart, Timeline, and Dashboard — likely related UI/navigation/mobile changes.

Poem

🐰
I nibble at hours, stitch minutes into song,
Claude hums a plan and the schedule grows strong,
A verification hop seals each careful thread,
Charts and timelines tidy where the rabbit led,
Little paws applaud — onward we hop along!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% 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 vague and uses a generic convention (feature branch name) rather than describing the actual changes; it doesn't convey what the AI feature accomplishes. Use a more descriptive title that explains the primary change, such as 'Add AI Assistant feature for schedule optimization' or 'Integrate AI-powered scheduling assistant with Claude backend'.
✅ 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 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 +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 +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.

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: 4

♻️ Duplicate comments (3)
src/components/Dashboard.tsx (1)

655-663: ⚠️ Potential issue | 🟠 Major

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

onApplySchedule applies blocks directly without the validation that manual block creation uses (validateTimeBlock, sortTimeBlocks). AI-generated blocks could contain overlaps or be unsorted, bypassing safeguards enforced elsewhere.

🛡️ Proposed validation
             onApplySchedule={(newBlocks) => {
-              setTimeBlocks(newBlocks);
+              const sorted = sortTimeBlocks(newBlocks);
+              const validated: TimeBlock[] = [];
+              for (const block of sorted) {
+                const { valid } = validateTimeBlock(
+                  { startTime: block.startTime, endTime: block.endTime },
+                  validated
+                );
+                if (valid) {
+                  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 655 - 663, onApplySchedule
currently sets AI-generated blocks directly (in the AIAssistant prop) which
bypasses manual-creation safeguards; instead validate each incoming block using
validateTimeBlock, discard or report any invalid blocks, then sort the resulting
list with sortTimeBlocks before calling setTimeBlocks (e.g., in the
onApplySchedule callback: run newBlocks.filter(validateTimeBlock) and then
sortTimeBlocks(...) and pass that to setTimeBlocks), ensuring you reference the
AIAssistant prop onApplySchedule, the validateTimeBlock function, sortTimeBlocks
function, and setTimeBlocks state updater.
src/services/aiService.ts (1)

65-77: ⚠️ Potential issue | 🟠 Major

Guard API response shape before mapping timeBlocks.

At Line 68, data.timeBlocks?.map(...) still throws if timeBlocks is a truthy non-array value. Validate payload shape first, then map.

Proposed fix
-    const data = await response.json();
+    const data = await response.json();

-    const enrichedBlocks = data.timeBlocks?.map(
+    const validBlocks = Array.isArray(data?.timeBlocks)
+      ? data.timeBlocks.filter(
+          (block: unknown): block is { startTime: string; endTime: string; label: string; color?: string } =>
+            !!block &&
+            typeof block === 'object' &&
+            typeof (block as any).startTime === 'string' &&
+            typeof (block as any).endTime === 'string' &&
+            typeof (block as any).label === 'string'
+        )
+      : undefined;
+
+    const enrichedBlocks = validBlocks?.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,
       })
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/aiService.ts` around lines 65 - 77, The code assumes
data.timeBlocks is an array when creating enrichedBlocks which will throw if
timeBlocks is a truthy non-array; change the mapping to first validate the shape
with Array.isArray(data.timeBlocks) (or a type guard) and only map when true,
otherwise set enrichedBlocks to an empty array or a sensible default so the
logic that uses enrichedBlocks won't crash; refer to the variables/enriched
mapping logic (data, timeBlocks, enrichedBlocks, crypto.randomUUID) and replace
the direct data.timeBlocks?.map(...) with a guarded expression like
Array.isArray(data.timeBlocks) ? data.timeBlocks.map(...) : [].
functions/src/index.ts (1)

68-83: ⚠️ Potential issue | 🟠 Major

Sanitize parsed schedule blocks before returning them.

At Line 76, parsed AI output is trusted without enforcing the file’s own scheduling rules (format, 5-minute increments, non-overlap). Invalid blocks can propagate downstream.

Proposed fix
 function 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 <= 59 && mm % 5 === 0;
+    };
+    const toMinutes = (t: string) => {
+      const [h, m] = t.split(":").map(Number);
+      return h * 60 + m;
+    };
+
+    const sanitized = parsed
+      .filter((b: any) =>
+        b &&
+        typeof b.startTime === "string" &&
+        typeof b.endTime === "string" &&
+        typeof b.label === "string" &&
+        isValidTime(b.startTime) &&
+        isValidTime(b.endTime) &&
+        toMinutes(b.startTime) < toMinutes(b.endTime)
+      )
+      .sort((a: any, b: any) => toMinutes(a.startTime) - toMinutes(b.startTime));
+
+    for (let i = 1; i < sanitized.length; i++) {
+      if (toMinutes(sanitized[i - 1].endTime) > toMinutes(sanitized[i].startTime)) {
+        return null;
+      }
+    }
+    if (sanitized.length === 0) return null;

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

In `@functions/src/index.ts` around lines 68 - 83, The parseScheduleFromResponse
function currently trusts the AI JSON; update it to sanitize and validate blocks
before returning: after parsing the array in parseScheduleFromResponse, add
helpers (e.g., isValidTime and toMinutes) to ensure startTime/endTime are
"HH:MM", minutes are multiples of 5, times are in-range, start < end, filter out
invalid entries, sort by start time, detect overlapping adjacent blocks (return
null if any overlap) and return null if the sanitized list is empty; then map
over the sanitized array (instead of the raw parsed array) to produce the
TimeBlock objects (keep fallback color logic) and preserve existing try/catch
behavior.
🧹 Nitpick comments (5)
.gitignore (1)

30-31: Consider if the specific pattern is needed.

The server/dist pattern may be redundant since the general dist pattern on line 11 already matches any directory named dist anywhere in the repository, including server/dist. However, keeping it can serve as documentation to clarify which build artifacts are being ignored.

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

In @.gitignore around lines 30 - 31, The line "server/dist" is likely redundant
because the existing "dist" pattern already ignores any directory named dist
(including server/dist); either remove the "server/dist" entry to avoid
duplication or keep it with an inline comment clarifying it's intentional
documentation for the server build artifact. Locate the .gitignore entries for
"dist" and "server/dist" and remove the latter or add a clarifying comment next
to "server/dist" indicating it's kept for documentation.
server/src/index.ts (2)

161-164: Add defensive check for empty response.content array.

Accessing response.content[0] assumes the array is non-empty. While unlikely with standard Anthropic responses, a defensive check prevents potential runtime errors.

🛡️ Proposed defensive check
-    const assistantText =
-      response.content[0].type === 'text'
-        ? response.content[0].text
-        : 'Sorry, I could not generate a response.';
+    const firstContent = response.content[0];
+    const assistantText =
+      firstContent?.type === 'text'
+        ? firstContent.text
+        : 'Sorry, I could not generate a response.';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 161 - 164, The current assignment to
assistantText reads response.content[0] without ensuring response.content exists
or has elements; add a defensive check before accessing index 0 (e.g., verify
response.content && response.content.length > 0) and only then evaluate
response.content[0].type === 'text', otherwise set assistantText to the fallback
message (or empty string). Update the logic where assistantText is computed (the
assignment using response.content[0]) to guard against undefined content and
preserve the existing fallback behavior.

88-107: Consider validating parsed schedule blocks for required fields and time format.

parseScheduleFromResponse parses JSON but doesn't validate that startTime/endTime are valid "HH:MM" strings or that required fields exist. Malformed AI output could propagate invalid data to the frontend.

♻️ Proposed validation
+const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/;
+
+function isValidTimeBlock(block: unknown): block is { startTime: string; endTime: string; label: string; color?: string } {
+  if (typeof block !== 'object' || block === null) return false;
+  const b = block as Record<string, unknown>;
+  return (
+    typeof b.startTime === 'string' && TIME_REGEX.test(b.startTime) &&
+    typeof b.endTime === 'string' && TIME_REGEX.test(b.endTime) &&
+    typeof b.label === 'string'
+  );
+}
+
 function 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 }) => ({
+    const validBlocks = parsed.filter(isValidTimeBlock);
+    if (validBlocks.length === 0) return null;
+
+    return validBlocks.map((block) => ({
       startTime: block.startTime,
       endTime: block.endTime,
       label: block.label,
       color: block.color || '#60a5fa',
-      })
-    );
+    }));
   } catch {
     return null;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 88 - 107, In parseScheduleFromResponse,
validate each parsed block before returning to avoid propagating malformed AI
output: after JSON.parse and confirming an array, filter entries using a helper
like isValidTimeBlock that ensures required fields (startTime, endTime, label)
exist and startTime/endTime match an HH:MM 24‑hour regex and that startTime <
endTime if applicable; if no valid blocks remain return null, otherwise map the
filtered blocks to the TimeBlock shape and apply the default color ('#60a5fa')
when color is missing.
src/components/ScheduleListPanel.tsx (1)

16-21: Consider adding accessibility attributes to the modal.

The modal lacks role, aria-modal, and aria-labelledby attributes. For better screen reader support, consider adding these attributes and implementing focus trapping.

♿ Proposed accessibility improvements
-    <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30 backdrop-blur-sm" onClick={onClose}>
+    <div 
+      className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30 backdrop-blur-sm" 
+      onClick={onClose}
+      role="dialog"
+      aria-modal="true"
+      aria-labelledby="schedule-panel-title"
+    >
       <div
         className="bg-white rounded-2xl shadow-2xl border border-gray-200 w-full max-w-lg max-h-[80vh] flex flex-col"
         onClick={(e) => e.stopPropagation()}
       >
         {/* Header */}
         <div className="flex items-center justify-between px-5 py-4 border-b border-gray-100">
           <div>
-            <h3 className="text-base font-semibold text-gray-900">Schedule Overview</h3>
+            <h3 id="schedule-panel-title" className="text-base font-semibold text-gray-900">Schedule Overview</h3>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ScheduleListPanel.tsx` around lines 16 - 21, The modal in
ScheduleListPanel is missing accessibility attributes and focus management;
update the outer container (the element with onClose) to include role="dialog"
and aria-modal="true", add aria-labelledby pointing to the modal title element
(give the title a stable id), and ensure the inner container (the element that
calls e.stopPropagation()) remains focusable; implement focus trapping and
initial focus via a React ref/useEffect inside ScheduleListPanel (also handle
Escape to call onClose) so screen readers and keyboard users can interact
correctly.
src/services/aiService.ts (1)

43-53: Consider adding an explicit request timeout for the AI call.

At Line 43, a stalled network call can hang the UI path for too long. Using AbortController here would improve resilience.

Proposed refactor
-    const response = await fetch(`${API_BASE_URL}/api/ai/message`, {
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 30000);
+    const response = await fetch(`${API_BASE_URL}/api/ai/message`, {
       method: 'POST',
+      signal: controller.signal,
       headers: {
         'Content-Type': 'application/json',
         'Authorization': `Bearer ${token}`,
       },
       body: JSON.stringify({
         messages,
         timeBlocks: currentTimeBlocks,
       }),
     });
+    clearTimeout(timeout);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/aiService.ts` around lines 43 - 53, Add an explicit request
timeout using AbortController around the fetch to /api/ai/message: create an
AbortController, pass controller.signal into the fetch options, start a
setTimeout that calls controller.abort() after a sensible timeout (e.g., 10s),
and clear the timer once fetch resolves; update the fetch error handling in the
same function (the fetch block that references API_BASE_URL, token, messages,
currentTimeBlocks) to treat aborts appropriately (propagate or map to a timeout
error) so the UI path won't hang on stalled network calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@functions/src/index.ts`:
- Around line 133-136: The code reads response.content[0] without checking the
array is non-empty, which can throw; update the assistantText assignment to
first safely obtain the first item (e.g., const first = response.content &&
response.content.length ? response.content[0] : undefined or use optional
chaining) and then check first?.type === "text" and first?.text for the value,
falling back to the existing default "Sorry, I could not generate a response.";
this change touches the assistantText assignment and the response.content
access.
- Around line 108-119: Validate the incoming request.data fully before using it:
ensure request.data conforms to SendAIMessageData by checking messages is an
array and each element has a valid role ("user" or "assistant") and non-empty
content, and ensure timeBlocks (if provided) is an array with the expected shape
before calling formatScheduleContext; if any check fails, throw an
HttpsError("invalid-argument", ...) with a clear message. Update the validation
around messages/timeBlocks where apiMessages is built and before calling
formatScheduleContext (referencing messages, timeBlocks, SendAIMessageData,
formatScheduleContext, and apiMessages) to prevent malformed items from reaching
the map and causing runtime errors.

In `@server/src/index.ts`:
- Around line 31-37: The server TimeBlock interface is missing the frontend
fields id and order; update the TimeBlock interface in server/src/index.ts to
include id: string and order: number, and then update any code that constructs,
validates, parses, or returns TimeBlock objects to preserve and use these fields
so incoming requests and responses match the frontend type (check places that
create/serialize TimeBlock instances after parsing or DB operations to ensure id
and order are maintained).
- Around line 15-22: The CORS middleware currently limits allowed methods to
['POST'] which blocks browser requests to the GET /health endpoint; update the
CORS configuration used in app.use(cors(...)) to include 'GET' (or include both
'GET' and 'POST') so the GET /health handler can be called from allowed origins
without preflight failures, ensuring the methods array passed to cors includes
'GET' when registering the middleware.

---

Duplicate comments:
In `@functions/src/index.ts`:
- Around line 68-83: The parseScheduleFromResponse function currently trusts the
AI JSON; update it to sanitize and validate blocks before returning: after
parsing the array in parseScheduleFromResponse, add helpers (e.g., isValidTime
and toMinutes) to ensure startTime/endTime are "HH:MM", minutes are multiples of
5, times are in-range, start < end, filter out invalid entries, sort by start
time, detect overlapping adjacent blocks (return null if any overlap) and return
null if the sanitized list is empty; then map over the sanitized array (instead
of the raw parsed array) to produce the TimeBlock objects (keep fallback color
logic) and preserve existing try/catch behavior.

In `@src/components/Dashboard.tsx`:
- Around line 655-663: onApplySchedule currently sets AI-generated blocks
directly (in the AIAssistant prop) which bypasses manual-creation safeguards;
instead validate each incoming block using validateTimeBlock, discard or report
any invalid blocks, then sort the resulting list with sortTimeBlocks before
calling setTimeBlocks (e.g., in the onApplySchedule callback: run
newBlocks.filter(validateTimeBlock) and then sortTimeBlocks(...) and pass that
to setTimeBlocks), ensuring you reference the AIAssistant prop onApplySchedule,
the validateTimeBlock function, sortTimeBlocks function, and setTimeBlocks state
updater.

In `@src/services/aiService.ts`:
- Around line 65-77: The code assumes data.timeBlocks is an array when creating
enrichedBlocks which will throw if timeBlocks is a truthy non-array; change the
mapping to first validate the shape with Array.isArray(data.timeBlocks) (or a
type guard) and only map when true, otherwise set enrichedBlocks to an empty
array or a sensible default so the logic that uses enrichedBlocks won't crash;
refer to the variables/enriched mapping logic (data, timeBlocks, enrichedBlocks,
crypto.randomUUID) and replace the direct data.timeBlocks?.map(...) with a
guarded expression like Array.isArray(data.timeBlocks) ?
data.timeBlocks.map(...) : [].

---

Nitpick comments:
In @.gitignore:
- Around line 30-31: The line "server/dist" is likely redundant because the
existing "dist" pattern already ignores any directory named dist (including
server/dist); either remove the "server/dist" entry to avoid duplication or keep
it with an inline comment clarifying it's intentional documentation for the
server build artifact. Locate the .gitignore entries for "dist" and
"server/dist" and remove the latter or add a clarifying comment next to
"server/dist" indicating it's kept for documentation.

In `@server/src/index.ts`:
- Around line 161-164: The current assignment to assistantText reads
response.content[0] without ensuring response.content exists or has elements;
add a defensive check before accessing index 0 (e.g., verify response.content &&
response.content.length > 0) and only then evaluate response.content[0].type ===
'text', otherwise set assistantText to the fallback message (or empty string).
Update the logic where assistantText is computed (the assignment using
response.content[0]) to guard against undefined content and preserve the
existing fallback behavior.
- Around line 88-107: In parseScheduleFromResponse, validate each parsed block
before returning to avoid propagating malformed AI output: after JSON.parse and
confirming an array, filter entries using a helper like isValidTimeBlock that
ensures required fields (startTime, endTime, label) exist and startTime/endTime
match an HH:MM 24‑hour regex and that startTime < endTime if applicable; if no
valid blocks remain return null, otherwise map the filtered blocks to the
TimeBlock shape and apply the default color ('#60a5fa') when color is missing.

In `@src/components/ScheduleListPanel.tsx`:
- Around line 16-21: The modal in ScheduleListPanel is missing accessibility
attributes and focus management; update the outer container (the element with
onClose) to include role="dialog" and aria-modal="true", add aria-labelledby
pointing to the modal title element (give the title a stable id), and ensure the
inner container (the element that calls e.stopPropagation()) remains focusable;
implement focus trapping and initial focus via a React ref/useEffect inside
ScheduleListPanel (also handle Escape to call onClose) so screen readers and
keyboard users can interact correctly.

In `@src/services/aiService.ts`:
- Around line 43-53: Add an explicit request timeout using AbortController
around the fetch to /api/ai/message: create an AbortController, pass
controller.signal into the fetch options, start a setTimeout that calls
controller.abort() after a sensible timeout (e.g., 10s), and clear the timer
once fetch resolves; update the fetch error handling in the same function (the
fetch block that references API_BASE_URL, token, messages, currentTimeBlocks) to
treat aborts appropriately (propagate or map to a timeout error) so the UI path
won't hang on stalled network calls.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66ad859 and 3a4a03c.

⛔ Files ignored due to path filters (2)
  • functions/package-lock.json is excluded by !**/package-lock.json
  • server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • .env.example
  • .gitignore
  • firebase.json
  • functions/package.json
  • functions/src/index.ts
  • functions/tsconfig.json
  • server/package.json
  • server/src/index.ts
  • server/tsconfig.json
  • src/components/Dashboard.tsx
  • src/components/ScheduleListPanel.tsx
  • src/firebase.ts
  • src/services/aiService.ts
✅ Files skipped from review due to trivial changes (1)
  • server/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • .env.example

Comment on lines +108 to +119
const { messages, timeBlocks } = request.data as SendAIMessageData;

if (!messages || !Array.isArray(messages)) {
throw new HttpsError("invalid-argument", "Messages array is required.");
}

const scheduleContext = formatScheduleContext(timeBlocks || []);

const apiMessages = messages.map((msg) => ({
role: msg.role as "user" | "assistant",
content: msg.content,
}));
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 request.data schema before processing.

At Line 108, only messages array existence is checked. Invalid timeBlocks and malformed message items can still flow into Line 114/Line 116 and fail at runtime.

Proposed fix
-    const { messages, timeBlocks } = request.data as SendAIMessageData;
+    const { messages, timeBlocks } = (request.data ?? {}) as Partial<SendAIMessageData>;

-    if (!messages || !Array.isArray(messages)) {
+    const isValidMessage = (m: unknown): m is ChatMessage =>
+      !!m &&
+      typeof m === "object" &&
+      ((m as ChatMessage).role === "user" || (m as ChatMessage).role === "assistant") &&
+      typeof (m as ChatMessage).content === "string";
+
+    if (!Array.isArray(messages) || !messages.every(isValidMessage)) {
       throw new HttpsError("invalid-argument", "Messages array is required.");
     }
+
+    if (timeBlocks != null && !Array.isArray(timeBlocks)) {
+      throw new HttpsError("invalid-argument", "timeBlocks must be an array.");
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@functions/src/index.ts` around lines 108 - 119, Validate the incoming
request.data fully before using it: ensure request.data conforms to
SendAIMessageData by checking messages is an array and each element has a valid
role ("user" or "assistant") and non-empty content, and ensure timeBlocks (if
provided) is an array with the expected shape before calling
formatScheduleContext; if any check fails, throw an
HttpsError("invalid-argument", ...) with a clear message. Update the validation
around messages/timeBlocks where apiMessages is built and before calling
formatScheduleContext (referencing messages, timeBlocks, SendAIMessageData,
formatScheduleContext, and apiMessages) to prevent malformed items from reaching
the map and causing runtime errors.

Comment on lines +133 to +136
const assistantText =
response.content[0].type === "text"
? response.content[0].text
: "Sorry, I could not generate a response.";
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

Avoid direct indexing into response.content without a guard.

At Line 134, response.content[0] can throw if the content array is empty. Add a safe lookup before reading .type.

Proposed fix
-      const assistantText =
-        response.content[0].type === "text"
-          ? response.content[0].text
-          : "Sorry, I could not generate a response.";
+      const firstPart = Array.isArray(response.content) ? response.content[0] : undefined;
+      const assistantText =
+        firstPart && firstPart.type === "text"
+          ? firstPart.text
+          : "Sorry, I could not generate a response.";
📝 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
const assistantText =
response.content[0].type === "text"
? response.content[0].text
: "Sorry, I could not generate a response.";
const firstPart = Array.isArray(response.content) ? response.content[0] : undefined;
const assistantText =
firstPart && firstPart.type === "text"
? firstPart.text
: "Sorry, I could not generate a response.";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@functions/src/index.ts` around lines 133 - 136, The code reads
response.content[0] without checking the array is non-empty, which can throw;
update the assistantText assignment to first safely obtain the first item (e.g.,
const first = response.content && response.content.length ? response.content[0]
: undefined or use optional chaining) and then check first?.type === "text" and
first?.text for the value, falling back to the existing default "Sorry, I could
not generate a response."; this change touches the assistantText assignment and
the response.content access.

Comment on lines +31 to +37
// Types
interface TimeBlock {
startTime: string;
endTime: string;
label: string;
color: string;
}
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

Server TimeBlock type is missing id and order fields.

The frontend TimeBlock type in src/types/schedule.ts includes id: string and order: number fields. The server type lacks these, which could cause type mismatches when the frontend sends blocks to the server or when parsed blocks are returned without these fields.

🛠️ Proposed fix to align with frontend type
 interface TimeBlock {
+  id?: string;
   startTime: string;
   endTime: string;
   label: string;
   color: string;
+  order?: number;
 }
📝 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
// Types
interface TimeBlock {
startTime: string;
endTime: string;
label: string;
color: string;
}
interface TimeBlock {
id?: string;
startTime: string;
endTime: string;
label: string;
color: string;
order?: number;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 31 - 37, The server TimeBlock interface is
missing the frontend fields id and order; update the TimeBlock interface in
server/src/index.ts to include id: string and order: number, and then update any
code that constructs, validates, parses, or returns TimeBlock objects to
preserve and use these fields so incoming requests and responses match the
frontend type (check places that create/serialize TimeBlock instances after
parsing or DB operations to ensure id and order are maintained).

@railway-app railway-app bot temporarily deployed to imaginative-perfection / production February 27, 2026 07:56 Inactive
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: 1

🧹 Nitpick comments (4)
server/src/index.ts (4)

10-12: Firebase Admin initialization may fail without credentials in non-GCP environments.

When running outside Google Cloud (e.g., local development), Firebase Admin SDK requires explicit credentials. Consider documenting the required environment setup or adding a check.

📝 Suggested improvement for local development support
 // Initialize Firebase Admin for auth token verification
+// Note: In GCP environments, credentials are auto-discovered.
+// For local development, set GOOGLE_APPLICATION_CREDENTIALS env var.
 admin.initializeApp({
   projectId: process.env.FIREBASE_PROJECT_ID,
 });

Or conditionally load credentials:

admin.initializeApp({
  projectId: process.env.FIREBASE_PROJECT_ID,
  credential: process.env.GOOGLE_APPLICATION_CREDENTIALS 
    ? admin.credential.applicationDefault() 
    : undefined,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 10 - 12, The Firebase Admin initialization
call (admin.initializeApp) can fail in non-GCP/local environments because
credentials may be missing; update the initialization to conditionally provide
credentials by detecting GOOGLE_APPLICATION_CREDENTIALS or similar env var and
pass admin.credential.applicationDefault() (or a parsed service account) when
present, otherwise omit credential to allow GCP default behavior, and add a
clear error log or throw if neither projectId nor credentials are available so
startup fails fast with guidance for local setup.

176-185: Error handling relies on string matching which is fragile.

Checking error.message.includes('authorization') may not catch all auth-related errors (e.g., token expired, invalid signature). Consider throwing a custom error type from verifyAuth for more reliable error classification.

💡 Alternative approach with custom error
class AuthError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AuthError';
  }
}

// In verifyAuth:
throw new AuthError('Missing or invalid authorization header');

// In catch block:
if (error instanceof AuthError) {
  res.status(401).json({ error: 'Unauthorized. Please sign in.' });
  return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 176 - 185, Replace fragile string matching
with a custom error type: add an AuthError class and have verifyAuth throw
AuthError for any auth failures (missing header, token expired, invalid
signature, etc.), then in the catch block in server/src/index.ts replace the
message.includes('authorization') check with an instanceof AuthError check to
return 401 and otherwise fall through to 500; update any call sites of
verifyAuth to ensure they propagate AuthError as thrown.

89-108: Consider validating parsed time block fields before use.

The function assumes the AI response contains properly structured objects with string fields. Malformed AI responses could lead to runtime issues when these values are used downstream (e.g., undefined.split(':') in time sorting).

🛡️ Add basic field validation
     return parsed.map(
-      (block: { startTime: string; endTime: string; label: string; color: string }) => ({
-        startTime: block.startTime,
-        endTime: block.endTime,
-        label: block.label,
-        color: block.color || '#60a5fa',
-      })
-    );
+      (block: unknown) => {
+        if (
+          typeof block !== 'object' ||
+          block === null ||
+          typeof (block as Record<string, unknown>).startTime !== 'string' ||
+          typeof (block as Record<string, unknown>).endTime !== 'string' ||
+          typeof (block as Record<string, unknown>).label !== 'string'
+        ) {
+          return null;
+        }
+        const b = block as { startTime: string; endTime: string; label: string; color?: string };
+        return {
+          startTime: b.startTime,
+          endTime: b.endTime,
+          label: b.label,
+          color: typeof b.color === 'string' ? b.color : '#60a5fa',
+        };
+      }
+    ).filter((b): b is TimeBlock => b !== null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 89 - 108, parseScheduleFromResponse assumes
each parsed block has valid string fields which can cause downstream runtime
errors; validate each object from JSON.parse inside parseScheduleFromResponse by
checking that startTime and endTime are non-empty strings matching a time
pattern (e.g., HH:MM), label is a string, and color is a string (fallback to
'#60a5fa' if missing), filter out any blocks that fail validation, and return
null or an empty array if no valid blocks remain; reference the
parseScheduleFromResponse function and the mapped block shape (startTime,
endTime, label, color) when making these checks.

150-160: Consider adding a timeout for the Anthropic API call.

External API calls without timeouts can cause requests to hang indefinitely, potentially exhausting server resources under load. The Anthropic SDK supports a timeout option.

⏱️ Add timeout configuration
-    const client = new Anthropic({ apiKey: claudeApiKey });
+    const client = new Anthropic({ 
+      apiKey: claudeApiKey,
+      timeout: 60000, // 60 second timeout
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 150 - 160, The Anthropic API call
(client.messages.create) currently has no timeout and can hang; add a timeout
option to the call (or when constructing the Anthropic client) so requests fail
fast—e.g., use a configurable ANTHROPIC_TIMEOUT_MS env var with a sensible
default (e.g., 30000 ms) and pass that value into the timeout option when
calling client.messages.create (or into new Anthropic({...})), ensuring the
timeout applies to the request that assigns to response and uses
claudeApiKey/model/SYSTEM_PROMPT/scheduleContext.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/src/index.ts`:
- Around line 162-165: The current assignment to assistantText directly indexes
response.content[0] (seen in the assistantText declaration) which can throw or
behave incorrectly if response.content is empty or undefined; change the logic
to first verify response.content exists and has at least one item and that that
item's type and text are defined (e.g., check response.content?.length > 0 and
response.content[0].type === 'text' and response.content[0].text) before using
it, otherwise use the fallback string—update the assistantText computation to
perform these defensive checks (or use optional chaining and a safe default) so
accessing response.content[0] cannot cause a runtime error.

---

Nitpick comments:
In `@server/src/index.ts`:
- Around line 10-12: The Firebase Admin initialization call
(admin.initializeApp) can fail in non-GCP/local environments because credentials
may be missing; update the initialization to conditionally provide credentials
by detecting GOOGLE_APPLICATION_CREDENTIALS or similar env var and pass
admin.credential.applicationDefault() (or a parsed service account) when
present, otherwise omit credential to allow GCP default behavior, and add a
clear error log or throw if neither projectId nor credentials are available so
startup fails fast with guidance for local setup.
- Around line 176-185: Replace fragile string matching with a custom error type:
add an AuthError class and have verifyAuth throw AuthError for any auth failures
(missing header, token expired, invalid signature, etc.), then in the catch
block in server/src/index.ts replace the message.includes('authorization') check
with an instanceof AuthError check to return 401 and otherwise fall through to
500; update any call sites of verifyAuth to ensure they propagate AuthError as
thrown.
- Around line 89-108: parseScheduleFromResponse assumes each parsed block has
valid string fields which can cause downstream runtime errors; validate each
object from JSON.parse inside parseScheduleFromResponse by checking that
startTime and endTime are non-empty strings matching a time pattern (e.g.,
HH:MM), label is a string, and color is a string (fallback to '#60a5fa' if
missing), filter out any blocks that fail validation, and return null or an
empty array if no valid blocks remain; reference the parseScheduleFromResponse
function and the mapped block shape (startTime, endTime, label, color) when
making these checks.
- Around line 150-160: The Anthropic API call (client.messages.create) currently
has no timeout and can hang; add a timeout option to the call (or when
constructing the Anthropic client) so requests fail fast—e.g., use a
configurable ANTHROPIC_TIMEOUT_MS env var with a sensible default (e.g., 30000
ms) and pass that value into the timeout option when calling
client.messages.create (or into new Anthropic({...})), ensuring the timeout
applies to the request that assigns to response and uses
claudeApiKey/model/SYSTEM_PROMPT/scheduleContext.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a4a03c and f10640d.

📒 Files selected for processing (1)
  • server/src/index.ts

Comment on lines +162 to +165
const assistantText =
response.content[0].type === 'text'
? response.content[0].text
: 'Sorry, I could not generate a response.';
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

Accessing response.content[0] without checking array length could throw.

If the API returns an empty content array, accessing index 0 would result in undefined, and the type check would fail silently, returning the fallback message. However, if the structure changes, this could cause runtime errors.

🛡️ Proposed defensive check
-    const assistantText =
-      response.content[0].type === 'text'
-        ? response.content[0].text
-        : 'Sorry, I could not generate a response.';
+    const firstContent = response.content[0];
+    const assistantText =
+      firstContent?.type === 'text'
+        ? firstContent.text
+        : 'Sorry, I could not generate a response.';
📝 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
const assistantText =
response.content[0].type === 'text'
? response.content[0].text
: 'Sorry, I could not generate a response.';
const firstContent = response.content[0];
const assistantText =
firstContent?.type === 'text'
? firstContent.text
: 'Sorry, I could not generate a response.';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 162 - 165, The current assignment to
assistantText directly indexes response.content[0] (seen in the assistantText
declaration) which can throw or behave incorrectly if response.content is empty
or undefined; change the logic to first verify response.content exists and has
at least one item and that that item's type and text are defined (e.g., check
response.content?.length > 0 and response.content[0].type === 'text' and
response.content[0].text) before using it, otherwise use the fallback
string—update the assistantText computation to perform these defensive checks
(or use optional chaining and a safe default) so accessing response.content[0]
cannot cause a runtime error.

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.

🧹 Nitpick comments (4)
src/services/aiService.ts (1)

25-31: Set this.name for proper error identification.

Custom error classes should set the name property for better debugging and error handling.

♻️ Proposed fix
 export class RateLimitError extends Error {
   code: string;
   constructor(message: string, code: string) {
     super(message);
+    this.name = 'RateLimitError';
     this.code = code;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/aiService.ts` around lines 25 - 31, The custom error class
RateLimitError doesn't set the error name; update the RateLimitError constructor
to assign this.name = 'RateLimitError' (after calling super) so stack traces and
instanceof checks properly identify the error; ensure the constructor still
accepts (message: string, code: string) and preserves this.code.
server/src/index.ts (2)

201-220: Consider validating AI-generated time blocks before returning.

parseScheduleFromResponse trusts AI output without validation. Invalid time formats or non-5-minute increments could propagate to the client. A basic validation pass would improve robustness.

🛡️ Suggested validation
 function 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]), mm = Number(m[2]);
+      return hh >= 0 && hh <= 23 && mm >= 0 && mm < 60 && mm % 5 === 0;
+    };
+
     return parsed.map(
       (block: { startTime: string; endTime: string; label: string; color: string }) => ({
         startTime: block.startTime,
         endTime: block.endTime,
         label: block.label,
         color: block.color || '#60a5fa',
       })
-    );
+    ).filter(b => isValidTime(b.startTime) && isValidTime(b.endTime));
   } catch {
     return null;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 201 - 220, parseScheduleFromResponse
currently trusts the AI JSON and returns blocks directly; add a validation pass
to ensure each block's startTime and endTime are valid "HH:MM" strings, hours
0–23, minutes 0–59 and divisible by 5, and that startTime < endTime (or discard
invalid blocks). Implement an isValidTime helper and use it to filter the mapped
results (also preserve the existing color fallback logic) so the function
returns only well-formed TimeBlock entries or null when none valid.

290-298: Input validation is incomplete for timeBlocks.

The endpoint validates messages is an array but doesn't validate timeBlocks. If a malformed timeBlocks is sent, formatScheduleContext could produce unexpected output or errors.

🛡️ Proposed validation
     const { messages, timeBlocks } = req.body as {
       messages: ChatMessage[];
       timeBlocks: TimeBlock[];
     };

     if (!messages || !Array.isArray(messages)) {
       res.status(400).json({ error: 'Messages array is required' });
       return;
     }
+
+    if (timeBlocks && !Array.isArray(timeBlocks)) {
+      res.status(400).json({ error: 'timeBlocks must be an array' });
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 290 - 298, The request currently validates
messages but not timeBlocks, which can lead to errors when calling
formatScheduleContext; add validation for req.body.timeBlocks similar to
messages: ensure timeBlocks exists, is an array, and optionally verify each
element matches the expected TimeBlock shape (e.g., has start/end or required
fields) before calling formatScheduleContext; on failure return
res.status(400).json({ error: 'timeBlocks array is required' }) (or a more
specific message) so formatScheduleContext receives well-formed input.
src/components/AIAssistant.tsx (1)

209-221: Consider replacing confirm() with a custom modal.

window.confirm is a synchronous blocking call that can be jarring on mobile and doesn't match the app's visual design. A custom confirmation modal would provide a better user experience.

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

In `@src/components/AIAssistant.tsx` around lines 209 - 221, Replace the
synchronous window.confirm call in handleApplySchedule with a non-blocking
custom confirmation modal: introduce local state (e.g., showConfirmModal) and
render a ConfirmModal (or your existing Modal component) that displays the same
warning and has Confirm/Cancel actions; change handleApplySchedule to open the
modal instead of calling confirm(), and move the onApplySchedule(blocks) call
and setMessages update into the modal's Confirm handler so the schedule is
applied only when the user confirms. Ensure the ConfirmModal exposes callbacks
(onConfirm, onCancel) and that accessibility/focus management is handled by the
modal component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/src/index.ts`:
- Around line 201-220: parseScheduleFromResponse currently trusts the AI JSON
and returns blocks directly; add a validation pass to ensure each block's
startTime and endTime are valid "HH:MM" strings, hours 0–23, minutes 0–59 and
divisible by 5, and that startTime < endTime (or discard invalid blocks).
Implement an isValidTime helper and use it to filter the mapped results (also
preserve the existing color fallback logic) so the function returns only
well-formed TimeBlock entries or null when none valid.
- Around line 290-298: The request currently validates messages but not
timeBlocks, which can lead to errors when calling formatScheduleContext; add
validation for req.body.timeBlocks similar to messages: ensure timeBlocks
exists, is an array, and optionally verify each element matches the expected
TimeBlock shape (e.g., has start/end or required fields) before calling
formatScheduleContext; on failure return res.status(400).json({ error:
'timeBlocks array is required' }) (or a more specific message) so
formatScheduleContext receives well-formed input.

In `@src/components/AIAssistant.tsx`:
- Around line 209-221: Replace the synchronous window.confirm call in
handleApplySchedule with a non-blocking custom confirmation modal: introduce
local state (e.g., showConfirmModal) and render a ConfirmModal (or your existing
Modal component) that displays the same warning and has Confirm/Cancel actions;
change handleApplySchedule to open the modal instead of calling confirm(), and
move the onApplySchedule(blocks) call and setMessages update into the modal's
Confirm handler so the schedule is applied only when the user confirms. Ensure
the ConfirmModal exposes callbacks (onConfirm, onCancel) and that
accessibility/focus management is handled by the modal component.

In `@src/services/aiService.ts`:
- Around line 25-31: The custom error class RateLimitError doesn't set the error
name; update the RateLimitError constructor to assign this.name =
'RateLimitError' (after calling super) so stack traces and instanceof checks
properly identify the error; ensure the constructor still accepts (message:
string, code: string) and preserves this.code.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 929ae11 and f3eb1ca.

📒 Files selected for processing (3)
  • server/src/index.ts
  • src/components/AIAssistant.tsx
  • src/services/aiService.ts

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