Skip to content

feat(timer): persist running timers and resume UI#60

Merged
grmbyrn merged 3 commits into
mainfrom
wl-47
Apr 3, 2026
Merged

feat(timer): persist running timers and resume UI#60
grmbyrn merged 3 commits into
mainfrom
wl-47

Conversation

@grmbyrn
Copy link
Copy Markdown
Owner

@grmbyrn grmbyrn commented Apr 3, 2026

Summary by CodeRabbit

  • New Features

    • Timers now automatically resume when returning to the app and prevent creating duplicate running timers.
  • Data

    • Time entries now include an explicit running/completed status and underlying database migration applied.
  • Tests

    • Added UI and API tests covering timer resume, start, stop, and conflict scenarios.
  • Chores

    • Consolidated Jest configuration and improved test runtime setup.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 3, 2026

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

Project Deployment Actions Updated (UTC)
worklog Ready Ready Preview, Comment Apr 3, 2026 11:29am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cdd57b49-c3a6-4450-99cb-422f4e4cb163

📥 Commits

Reviewing files that changed from the base of the PR and between 4e2d474 and b714e12.

📒 Files selected for processing (5)
  • __tests__/TimerPage.resume.test.tsx
  • __tests__/api/timer.post.test.ts
  • app/timer/page.tsx
  • jest.setup.ts
  • prisma/migrations/20260403085905_add_timeentry_status/migration.sql
✅ Files skipped from review due to trivial changes (3)
  • tests/TimerPage.resume.test.tsx
  • prisma/migrations/20260403085905_add_timeentry_status/migration.sql
  • tests/api/timer.post.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/timer/page.tsx
  • jest.setup.ts

📝 Walkthrough

Walkthrough

Adds a TimeEntryStatus enum and status column, updates timer API handlers to use explicit statuses and ownership checks, resumes running timers in the TimerPage on mount, and introduces Jest config, test setup, and tests covering GET/POST/PUT timer flows and UI resume.

Changes

Cohort / File(s) Summary
Database Schema & Migration
prisma/schema.prisma, prisma/migrations/.../migration.sql
Add TimeEntryStatus enum (RUNNING, COMPLETED) and new non-null TimeEntry.status with default RUNNING; backfill existing rows to COMPLETED where endTime is set.
API Endpoint Handlers
app/api/timer/route.ts, app/api/timer/[id]/route.ts
GET now returns a single runningEntry by status === RUNNING; POST prevents creating a new running entry if one exists (returns 409) and sets status: RUNNING; PUT verifies existence/ownership before updating and sets status: COMPLETED.
Frontend Timer Resume
app/timer/page.tsx
Add mount-time effect that calls GET /api/timer, and if runningEntry exists restores selectedClientId, activeEntryId, startTime, elapsed seconds, and isRunning.
Jest Configuration & Setup
jest.config.js, jest.config.ts, jest.setup.ts
Replace jest.config.ts with jest.config.js for jsdom + ts-jest setup; update moduleNameMapper; enhance jest.setup.ts with typed globals (TextEncoder/Decoder), TestRequest/TestResponse shims, and default env vars.
Tests
__tests__/TimerPage.resume.test.tsx, __tests__/api/timer.get.test.ts, __tests__/api/timer.post.test.ts, __tests__/api/timer.put.test.ts
Add tests covering UI resume behavior, GET returning running/null entries, POST creating or rejecting new running entries, and PUT completing or reporting missing entries; extensive Prisma and session mocks included.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser (TimerPage)
  participant API as Server (app/api/timer)
  participant DB as Database (Prisma)

  Browser->>API: GET /api/timer
  API->>DB: timeEntry.findFirst(status: RUNNING, include: client)
  DB-->>API: runningEntry | null
  API-->>Browser: 200 { runningEntry }
  alt runningEntry returned
    Browser->>Browser: set selectedClientId, activeEntryId, startTime, isRunning
  end

  Browser->>API: POST /api/timer { clientId, startTime }
  API->>DB: timeEntry.findFirst(status: RUNNING, userId)
  alt existing RUNNING
    API-->>Browser: 409 { error: "A timer is already running" }
  else no running
    API->>DB: timeEntry.create({ status: RUNNING, ... })
    DB-->>API: new timeEntry
    API-->>Browser: 201 { timeEntry }
  end

  Browser->>API: PUT /api/timer/{id} { endTime }
  API->>DB: timeEntry.findUnique(id)
  alt not found or wrong user
    API-->>Browser: 404 { error: "Time entry not found" }
  else found
    API->>DB: timeEntry.update(id, { status: COMPLETED, endTime })
    DB-->>API: updated timeEntry
    API-->>Browser: 200 { timeEntry }
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped to check the running time,
Found a tick that still did chime.
Status set and tests in place,
I nibbled bugs without a trace.
Cheers — the timer's back in race! ⏱️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(timer): persist running timers and resume UI' accurately and concisely describes the main changes: adding persistence for running timers and resuming them in the UI.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wl-47

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
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/timer/[id]/route.ts (1)

33-49: ⚠️ Potential issue | 🟠 Major

Handle P2025 error to return correct 404 status on race condition.

Lines 33–38 are non-atomic: if the time entry is deleted between the ownership check and the update call, Prisma throws P2025 but the catch block returns 500 instead of 404. Add explicit error handling to map P2025 to a 404 response.

Suggested fix
-import { TimeEntryStatus } from '@prisma/client';
+import { Prisma, TimeEntryStatus } from '@prisma/client';
@@
-  } catch (error) {
+  } catch (error: unknown) {
+    if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
+      return Response.json({ error: 'Time entry not found' }, { status: 404 });
+    }
     console.error('Error updating time entry:', error);
     return Response.json({ error: 'Internal server error' }, { status: 500 });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/timer/`[id]/route.ts around lines 33 - 49, The update flow currently
does a non-atomic check with prisma.timeEntry.findUnique followed by
prisma.timeEntry.update, and if the row is deleted in between Prisma throws a
P2025 but the catch block returns 500; update the catch in the route handler to
detect Prisma.PrismaClientKnownRequestError (or inspect error.code === 'P2025')
and return Response.json({ error: 'Time entry not found' }, { status: 404 }) for
that case, otherwise keep the existing 500 handling and console.error; reference
prisma.timeEntry.findUnique, prisma.timeEntry.update and the current catch block
to locate where to add this mapping.
🧹 Nitpick comments (4)
__tests__/api/timer.put.test.ts (1)

37-42: Assert response status codes for route-contract coverage.

Both cases verify body only. Add status assertions so regressions in HTTP semantics (e.g., 404 vs 500) are caught.

Suggested additions
     const res = await PUT(req as Request, { params: Promise.resolve({ id: 'entry1' }) } as { params: Promise<{ id: string }> });
     const json = await res.json();

+    expect(res.status).toBe(200);
     expect(json.timeEntry).toBeDefined();
     expect(json.timeEntry.status).toBe('COMPLETED');
@@
     const res = await PUT(req as Request, { params: Promise.resolve({ id: 'doesnotexist' }) } as { params: Promise<{ id: string }> });
     const json = await res.json();

+    expect(res.status).toBe(404);
     expect(json.error).toBe('Time entry not found');

Also applies to: 54-58

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

In `@__tests__/api/timer.put.test.ts` around lines 37 - 42, Tests currently only
assert the response body; add explicit status assertions to catch HTTP-semantic
regressions. After calling PUT(...) and before examining json, assert the
response status (e.g., expect(res.status).toBe(200)) in the block that checks
json.timeEntry.status === 'COMPLETED', and do the same for the other test case
around lines 54-58; update the assertions to reference the same res variable
returned by the PUT handler to ensure both status and body are validated.
prisma/schema.prisma (1)

41-53: Add an index for the new running-timer lookup path.

Given queries filter by userId + status, add a composite index on TimeEntry to avoid table scans as entries grow.

Suggested schema update
 model TimeEntry {
   id        String    `@id` `@default`(cuid())
   startTime DateTime
   endTime   DateTime?
   status    TimeEntryStatus `@default`(RUNNING)
   createdAt DateTime  `@default`(now())
   
   clientId  String
   client    Client    `@relation`(fields: [clientId], references: [id], onDelete: Cascade)
   
   userId    String
   user      User      `@relation`(fields: [userId], references: [id], onDelete: Cascade)
+
+  @@index([userId, status])
 }

Also applies to: 55-58

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

In `@prisma/schema.prisma` around lines 41 - 53, Add a composite index on the
TimeEntry model for the lookup path used in queries (userId + status): update
the TimeEntry model in prisma/schema.prisma (the model named TimeEntry) to
declare a model-level index (@@index) covering the userId and status fields so
queries filtering by userId and status use the index and avoid table scans;
include an explicit index name if desired for clarity (e.g.,
idx_timeentry_userid_status) and run prisma migrate to apply the change.
jest.config.js (1)

6-10: Consider centralizing alias mappings to reduce maintenance drift.

Lines 14–15 manually mirror the tsconfig.json paths (lines 21–23 in tsconfig.json). Although currently in sync, this duplication can silently drift when aliases change. If these mappings must remain separate, document or automate the synchronization (e.g., via a shared configuration or build step) to maintain a single source of truth.

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

In `@jest.config.js` around lines 6 - 10, The jest config currently duplicates
tsconfig path aliases inside the '^.+\\.(ts|tsx)$' transformer ts-jest options,
which risks drift; remove the inline paths object and instead make ts-jest read
aliases from the single tsconfig.json (or load paths programmatically) so
aliases are centralized—update the transformer config for 'ts-jest' to reference
the project's tsconfig (or use a small helper that reads tsconfig.json and
injects paths), or alternatively add automation/documentation to keep the
ts-jest paths in sync with tsconfig.json; target the transformer config block
for '^.+\\.(ts|tsx)$' and the tsconfig.json paths definitions when making this
change.
app/api/timer/route.ts (1)

89-98: Inconsistent status value type: use TimeEntryStatus.RUNNING for consistency.

The POST handler uses the TimeEntryStatus.RUNNING enum (Line 46, 60), but the GET handler uses the string literal 'RUNNING'. While Prisma accepts both, using the enum consistently improves type safety and maintainability.

♻️ Proposed fix
     const runningEntry = await prisma.timeEntry.findFirst({
       where: {
         userId: user.id,
-        status: 'RUNNING',
+        status: TimeEntryStatus.RUNNING,
       },
       include: {
         client: true,
       },
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/timer/route.ts` around lines 89 - 98, Replace the string literal
status filter with the enum in the GET handler: in the
prisma.timeEntry.findFirst call (the runningEntry query) use
TimeEntryStatus.RUNNING instead of 'RUNNING' to match the POST handler and
maintain type safety and consistency with the TimeEntryStatus enum.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/api/timer.post.test.ts`:
- Around line 20-26: The test's mocked module path is inconsistent: change the
requireMock call so the mocked prisma module is actually imported (replace
"../../app/lib/prisma" with the same module ID used in jest.mock, i.e.,
"@/lib/prisma") so that the destructured const { prisma: mockPrisma } refers to
the real Jest mock; update the requireMock invocation near the top of the test
(the statement that defines mockPrisma) to use "@/lib/prisma" to match the mock
declaration used elsewhere (see how timer.get.test.ts imports the same mock).

In `@__tests__/TimerPage.resume.test.tsx`:
- Around line 31-34: The assertion currently only checks for any colon-delimited
text using waitFor and screen.getByText, which can match "00:00:00"; update the
waitFor assertion to ensure the displayed time is not all zeros by matching a
regex that excludes "00:00:00" (for example use waitFor(() =>
expect(screen.getByText(/(?!00:00:00)\d{2}:\d{2}:\d{2}/)).toBeInTheDocument())),
i.e., replace the /:/ check with a stricter regex or an explicit
expect(...).not.toHaveTextContent('00:00:00') using the existing waitFor and
screen.getByText helpers.

In `@app/timer/page.tsx`:
- Around line 48-49: Clamp the computed elapsed seconds to non-negative before
updating state: when computing diff from Date.now() - s.getTime() (the current
elapsed calculation used before calling setSeconds), wrap the result with
Math.max(0, ...) so any negative value (due to clock skew or a future start
time) becomes 0, then call setSeconds with that clamped value.

In `@jest.setup.ts`:
- Around line 34-35: Guard the global fetch API shims with feature-detection
instead of unconditional overwrite: check typeof global.Request === 'undefined'
before assigning (global as unknown as { Request?: unknown }).Request =
TestRequest, and check typeof global.Response === 'undefined' before assigning
the Response mock; also update the Response mock used in the Response assignment
to include an .ok boolean and any other properties your code expects (e.g.,
status, json/text methods) so calls like if (!res.ok) in app/timer/page.tsx,
app/dashboard/page.tsx, app/invoices/page.tsx behave correctly.

In `@prisma/migrations/20260403085905_add_timeentry_status/migration.sql`:
- Line 5: The migration currently sets every TimeEntry.status to 'RUNNING' which
incorrectly marks entries with a non-null endTime as active; after adding the
"status" column (enum TimeEntryStatus) keep the default but run a corrective
UPDATE: set "status" = 'COMPLETED' WHERE "endTime" IS NOT NULL (targeting the
"TimeEntry" table and the "endTime" column), so existing completed rows are
marked correctly; ensure this UPDATE is executed in the same migration before
any NOT NULL constraints or assumptions about status.

---

Outside diff comments:
In `@app/api/timer/`[id]/route.ts:
- Around line 33-49: The update flow currently does a non-atomic check with
prisma.timeEntry.findUnique followed by prisma.timeEntry.update, and if the row
is deleted in between Prisma throws a P2025 but the catch block returns 500;
update the catch in the route handler to detect
Prisma.PrismaClientKnownRequestError (or inspect error.code === 'P2025') and
return Response.json({ error: 'Time entry not found' }, { status: 404 }) for
that case, otherwise keep the existing 500 handling and console.error; reference
prisma.timeEntry.findUnique, prisma.timeEntry.update and the current catch block
to locate where to add this mapping.

---

Nitpick comments:
In `@__tests__/api/timer.put.test.ts`:
- Around line 37-42: Tests currently only assert the response body; add explicit
status assertions to catch HTTP-semantic regressions. After calling PUT(...) and
before examining json, assert the response status (e.g.,
expect(res.status).toBe(200)) in the block that checks json.timeEntry.status ===
'COMPLETED', and do the same for the other test case around lines 54-58; update
the assertions to reference the same res variable returned by the PUT handler to
ensure both status and body are validated.

In `@app/api/timer/route.ts`:
- Around line 89-98: Replace the string literal status filter with the enum in
the GET handler: in the prisma.timeEntry.findFirst call (the runningEntry query)
use TimeEntryStatus.RUNNING instead of 'RUNNING' to match the POST handler and
maintain type safety and consistency with the TimeEntryStatus enum.

In `@jest.config.js`:
- Around line 6-10: The jest config currently duplicates tsconfig path aliases
inside the '^.+\\.(ts|tsx)$' transformer ts-jest options, which risks drift;
remove the inline paths object and instead make ts-jest read aliases from the
single tsconfig.json (or load paths programmatically) so aliases are
centralized—update the transformer config for 'ts-jest' to reference the
project's tsconfig (or use a small helper that reads tsconfig.json and injects
paths), or alternatively add automation/documentation to keep the ts-jest paths
in sync with tsconfig.json; target the transformer config block for
'^.+\\.(ts|tsx)$' and the tsconfig.json paths definitions when making this
change.

In `@prisma/schema.prisma`:
- Around line 41-53: Add a composite index on the TimeEntry model for the lookup
path used in queries (userId + status): update the TimeEntry model in
prisma/schema.prisma (the model named TimeEntry) to declare a model-level index
(@@index) covering the userId and status fields so queries filtering by userId
and status use the index and avoid table scans; include an explicit index name
if desired for clarity (e.g., idx_timeentry_userid_status) and run prisma
migrate to apply the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d4cb7f5d-2f3e-41de-a21b-077147fcfffe

📥 Commits

Reviewing files that changed from the base of the PR and between 7f0cd9c and 4e2d474.

📒 Files selected for processing (12)
  • __tests__/TimerPage.resume.test.tsx
  • __tests__/api/timer.get.test.ts
  • __tests__/api/timer.post.test.ts
  • __tests__/api/timer.put.test.ts
  • app/api/timer/[id]/route.ts
  • app/api/timer/route.ts
  • app/timer/page.tsx
  • jest.config.js
  • jest.config.ts
  • jest.setup.ts
  • prisma/migrations/20260403085905_add_timeentry_status/migration.sql
  • prisma/schema.prisma
💤 Files with no reviewable changes (1)
  • jest.config.ts

Comment thread __tests__/api/timer.post.test.ts Outdated
Comment thread __tests__/TimerPage.resume.test.tsx Outdated
Comment thread app/timer/page.tsx Outdated
Comment thread jest.setup.ts
Comment thread prisma/migrations/20260403085905_add_timeentry_status/migration.sql
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
jest.setup.ts (1)

34-34: ⚠️ Potential issue | 🟠 Major

Remove unconditional global Request override.

Line 34 overwrites global.Request before the feature-detection block, so Line 66 becomes ineffective and native Request is always clobbered.

Proposed fix
-(global as unknown as { Request?: unknown }).Request = TestRequest;
-
 // Basic env required by validateEnv and next-auth during tests
 process.env.DATABASE_URL = process.env.DATABASE_URL || 'postgresql://test:test@localhost:5432/test';
 process.env.NEXTAUTH_SECRET = process.env.NEXTAUTH_SECRET || 'test-secret';
 process.env.NEXTAUTH_URL = process.env.NEXTAUTH_URL || 'http://localhost:3000';
#!/bin/bash
# Verify there is no unconditional Request assignment left and only guarded assignment remains.
rg -n -C2 '\bRequest\s*=\s*TestRequest' jest.setup.ts
rg -n -C2 'if \(\(global as unknown as \{ Request\?: unknown \}\)\.Request === undefined\)' jest.setup.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jest.setup.ts` at line 34, Remove the unconditional override of
global.Request so native Request isn't always clobbered: delete the direct
assignment (global as unknown as { Request?: unknown }).Request = TestRequest
and keep the feature-detection guarded assignment that checks if (global as
unknown as { Request?: unknown }).Request === undefined before assigning
TestRequest; ensure only the guarded path sets Request and that TestRequest is
assigned there.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@jest.setup.ts`:
- Around line 21-31: The TestRequest.json() and text() implementations need to
match Fetch semantics: in json(), change the falsy check to strict `this._body
=== undefined` to allow empty-string bodies, and do not swallow parse errors—let
JSON.parse throw (remove the try/catch that returns the raw body) so malformed
JSON surfaces; in text(), always return a string (e.g., coerce `this._body` to a
string when resolving) so it never returns undefined. Reference: the
TestRequest.json() and TestRequest.text() methods and the `this._body` field.

---

Duplicate comments:
In `@jest.setup.ts`:
- Line 34: Remove the unconditional override of global.Request so native Request
isn't always clobbered: delete the direct assignment (global as unknown as {
Request?: unknown }).Request = TestRequest and keep the feature-detection
guarded assignment that checks if (global as unknown as { Request?: unknown
}).Request === undefined before assigning TestRequest; ensure only the guarded
path sets Request and that TestRequest is assigned there.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 785cb4ec-dc2c-4d9e-993a-cb4694b2ec76

📥 Commits

Reviewing files that changed from the base of the PR and between 4e2d474 and edb439e.

📒 Files selected for processing (5)
  • __tests__/TimerPage.resume.test.tsx
  • __tests__/api/timer.post.test.ts
  • app/timer/page.tsx
  • jest.setup.ts
  • prisma/migrations/20260403085905_add_timeentry_status/migration.sql
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/timer/page.tsx
  • prisma/migrations/20260403085905_add_timeentry_status/migration.sql
  • tests/TimerPage.resume.test.tsx
  • tests/api/timer.post.test.ts

Comment thread jest.setup.ts
@grmbyrn grmbyrn merged commit 7f6e1cf into main Apr 3, 2026
4 checks passed
@grmbyrn grmbyrn deleted the wl-47 branch April 3, 2026 11:44
@coderabbitai coderabbitai Bot mentioned this pull request Apr 3, 2026
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