Skip to content

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Jan 14, 2026

Add a Tasks button to the left sidebar that shows all tasks assigned to the current user, with scoping similar to activity feeds:

  • Dashboard level: Shows tasks across all drives
  • Drive level: Shows tasks within that specific drive

Changes:

  • Add Tasks button to left sidebar (next to Activity)
  • Create /api/tasks endpoint for scoped task queries
  • Add TasksDashboard component with filtering by status/priority/drive
  • Add getDriveIdsForUser permission helper function
  • Create /dashboard/tasks and /dashboard/[driveId]/tasks pages

Summary by CodeRabbit

  • New Features
    • Added a centralized Tasks dashboard (user and drive contexts) with drive-specific pages.
    • New Tasks sidebar menu item for quick access.
    • Filters for status, priority and drive; URL-synced filtering.
    • Task grouping, enriched task details (drive/page links), and "Load more" pagination.
    • Loading skeletons, refresh control, and descriptive error states.

✏️ Tip: You can customize this high-level summary in your review settings.

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

Adds a Tasks feature: a GET /api/tasks endpoint with auth, authorization, validation, filtering, enrichment and pagination; comprehensive contract tests; user and drive task pages; a TasksDashboard UI with filters and grouping; task types; and a permission helper to list drives accessible to a user.

Changes

Cohort / File(s) Summary
API Route & Tests
apps/web/src/app/api/tasks/route.ts, apps/web/src/app/api/tasks/__tests__/route.test.ts
New GET /api/tasks handler with JWT/MCP auth, Zod query validation, drive-access enforcement, task filtering (status/priority/date), pagination, enrichment (driveId, taskListPageId, taskListPageTitle), error handling, and extensive contract tests covering auth, authorization, validation, retrieval, filtering, pagination, and error paths.
Frontend Pages
apps/web/src/app/dashboard/tasks/page.tsx, apps/web/src/app/dashboard/[driveId]/tasks/page.tsx
New user-wide and drive-specific task pages that fetch drives, show loading skeletons, and render the TasksDashboard with appropriate context and params; Suspense-based fallbacks.
Tasks Component & Types
apps/web/src/components/tasks/TasksDashboard.tsx, apps/web/src/components/tasks/index.ts, apps/web/src/components/tasks/types.ts
New TasksDashboard client component with filters, pagination, status grouping, URL sync, task rendering, and skeletons; new task-related types and an index barrel export.
Navigation UI
apps/web/src/components/layout/left-sidebar/index.tsx
Added "Tasks" navigation item (CheckSquare icon) linking to drive-specific or user-wide tasks based on drive context.
Permissions Utility Exports
packages/lib/src/index.ts, packages/lib/src/permissions/index.ts, packages/lib/src/permissions/permissions.ts
Added and exported getDriveIdsForUser(userId) which aggregates drives owned, drives where the user is a member, and drives with page-level view permissions, returning deduplicated drive IDs; updated barrels to export it.

Sequence Diagram(s)

sequenceDiagram
    actor Browser
    participant Page as Tasks Page
    participant API as /api/tasks
    participant Auth as Auth Service
    participant Perm as Permissions (getDriveIdsForUser)
    participant DB as Database
    participant UI as TasksDashboard

    Browser->>Page: Navigate to tasks page
    Page->>API: GET /api/tasks?context=...
    API->>Auth: Validate token (JWT/MCP)
    Auth-->>API: Authenticated (userId)
    API->>Perm: getDriveIdsForUser(userId)
    Perm->>DB: Query drives, driveMembers, pagePermissions
    DB-->>Perm: driveId list
    Perm-->>API: accessible driveIds
    API->>DB: Fetch task list pages for drives
    DB-->>API: pages & taskLists
    API->>DB: Query tasks with relations + count (filters, limit/offset)
    DB-->>API: tasks + total
    API->>API: Enrich tasks with drive/page titles
    API-->>Page: { tasks, total, limit, offset, hasMore }
    Page->>UI: Render tasks with grouping, filters, pagination
    UI->>Browser: Display tasks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I hopped through code to fetch each task and name,
Carrots of filters, pagination, and frame.
With drives all gathered and pages aligned,
I nibbled bugs, left tests—neat and refined.
Hop on, tasks, let productivity reign!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% 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 PR title clearly describes the main changes: adding a Tasks sidebar button and implementing scoped task views (user-level and drive-level dashboards), which aligns directly with the core deliverables in the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
apps/web/src/app/api/tasks/route.ts (2)

155-166: Consider using a Map for O(1) lookups instead of repeated .find() calls.

The current implementation uses .find() on taskListPages for each entry in taskListsData, resulting in O(n×m) complexity. For larger datasets, this could be optimized with a pre-built Map.

♻️ Suggested optimization
+    // Build map from pageId to page info for O(1) lookup
+    const pageIdToInfoMap = new Map(taskListPages.map(p => [p.id, p]));
+
     // Build map of taskListId -> page info for enriching results
     const taskListToPageMap = new Map<string, { pageId: string; driveId: string; taskListTitle: string }>();
     for (const tl of taskListsData) {
-      const pageInfo = taskListPages.find(p => p.id === tl.pageId);
+      const pageInfo = pageIdToInfoMap.get(tl.pageId);
       if (pageInfo) {
         taskListToPageMap.set(tl.id, {
           pageId: pageInfo.id,
           driveId: pageInfo.driveId,
           taskListTitle: pageInfo.title,
         });
       }
     }

168-173: Consider scoping trashed pages query to accessible drives.

The current query fetches all trashed pages across the entire database. For large databases, this could be inefficient. Since tasks are already scoped to accessible drives, the trashed page exclusion could be similarly scoped.

♻️ Suggested optimization
     // Get trashed page IDs to exclude tasks referencing them
     // This ensures pagination counts match the actual filtered results
     const trashedPages = await db.select({ id: pages.id })
       .from(pages)
-      .where(eq(pages.isTrashed, true));
+      .where(and(
+        eq(pages.isTrashed, true),
+        inArray(pages.driveId, driveIds)
+      ));
     const trashedPageIds = trashedPages.map(p => p.id);

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25035ee and bbfb271.

📒 Files selected for processing (1)
  • apps/web/src/app/api/tasks/route.ts
🧰 Additional context used
📓 Path-based instructions (5)
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/tasks/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/app/api/tasks/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/app/api/tasks/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/tasks/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/app/api/tasks/route.ts
🧠 Learnings (3)
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : In Route Handlers, get search parameters with `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Get search parameters using `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
🔇 Additional comments (8)
apps/web/src/app/api/tasks/route.ts (8)

1-22: LGTM!

Imports are correctly structured using zod/v4 path for Zod 4, and the query schema properly uses z.coerce for automatic type coercion of URL parameters. The pagination defaults (limit: 50, offset: 0) and constraints (min: 1, max: 100 for limit) are reasonable.


31-60: LGTM!

Authentication follows the established pattern, and search parameters are correctly extracted using const { searchParams } = new URL(request.url); per coding guidelines. The Zod validation with safeParse properly returns 400 for invalid input.


62-109: LGTM!

Authorization logic correctly uses centralized permission functions (isUserDriveMember, getDriveIdsForUser) from @pagespace/lib as per coding guidelines. Both drive and user contexts properly validate access and return appropriate 403 responses for unauthorized requests.


175-200: LGTM!

Filter conditions correctly apply trashed page exclusion at the database level, which aligns with the pagination fix mentioned in PR objectives. The date range handling properly makes the end date inclusive by adding one day. The assigneeId filter appropriately scopes tasks to the authenticated user.


202-225: LGTM!

The tasks query efficiently fetches related data in a single query using Drizzle's with clause, properly applies the filter conditions, and handles pagination at the database level.


227-246: Good defensive handling of orphaned tasks.

The orphaned task filtering with warning logs is good defensive coding that addresses the past review comment about undefined driveId. The type guard filter (task): task is NonNullable<typeof task> properly maintains type safety.


248-264: Note: Pagination metadata may be slightly off if orphaned tasks exist.

The count query correctly uses the same whereCondition as the tasks query. However, since orphaned task filtering (lines 232-237) happens post-query, if data inconsistencies exist, total could include tasks that get filtered out from enrichedTasks, causing minor pagination inaccuracies.

Given the defensive nature of this filtering (should rarely trigger with consistent data) and the warning logs that surface issues, this is acceptable. The warning logs at line 233-236 will help identify if data integrity issues need addressing.


265-271: LGTM!

Error handling properly logs the error for debugging while returning a generic message to avoid leaking internal details.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

@2witstudios
Copy link
Owner Author

🔬 Code Review Summary

Reviewer: Claude Code
Branch: claude/add-tasks-sidebar-button-Sl1l5
Overall Score: 85/100 (Good)

✅ Strengths

  • Clean Feature Implementation: Tasks dashboard with proper type separation and exports
  • Excellent Security Coverage: WebSocket auth migration with comprehensive OWASP compliance
  • Good Test Updates: WebSocket security tests updated for new challenge-response flow
  • Proper Authorization: Tasks API enforces drive membership checks

🟠 Important Issues (Should Address)

Issue File Description
Missing Tests apps/web/src/app/api/tasks/route.ts New 254-line endpoint with no test coverage
Inconsistent Sanitization apps/web/src/app/api/ai/global/[id]/messages/route.ts Missing Gemini tool name sanitization (colons in MCP tool names)

🟡 Minor Issues

Issue File Description
Potential N+1 Query apps/web/src/app/api/tasks/route.ts 4 sequential queries - consider optimization if perf issues arise
Inline Sanitization apps/web/src/app/api/ai/chat/route.ts Consider extracting Gemini sanitization to shared utility

Security Review (OWASP Top 10) ✅

  • A01 Broken Access Control: ✅ Proper authorization checks
  • A02 Cryptographic Failures: ✅ SHA256 for challenge-response
  • A03 Injection: ✅ Zod validation + parameterized queries
  • A04 Insecure Design: ✅ Challenge-response with expiry + brute force protection
  • A07 Auth Failures: ✅ JWT + challenge verification
  • A09 Logging: ✅ Comprehensive security event logging

Recommendation

Approved with conditions - Merge after:

  1. Adding basic authorization tests for /api/tasks endpoint
  2. Verifying Gemini MCP tool compatibility in global assistant route

Detailed comments added below on specific files.

@2witstudios
Copy link
Owner Author

🟠 Important: Missing Test Coverage

File: apps/web/src/app/api/tasks/route.ts

This new 254-line API endpoint has no test coverage. For a data-access endpoint handling task information across drives, tests are critical.

Recommended test cases:

describe('GET /api/tasks', () => {
  // Authorization
  it('should return 403 for inaccessible drive');
  it('should return tasks only from user-accessible drives');
  
  // Pagination
  it('should respect limit and offset parameters');
  it('should return hasMore=true when more tasks exist');
  
  // Filters
  it('should filter by status');
  it('should filter by priority');
  it('should combine multiple filters');
  
  // Edge cases
  it('should return empty array when no tasks exist');
  it('should exclude tasks from trashed pages');
});

@2witstudios
Copy link
Owner Author

Code review

Found 2 issues:

  1. Pagination count mismatch: Tasks are filtered client-side to exclude trashed pages (filteredTasks = tasks.filter(task => !task.page?.isTrashed)), but the total count query does not apply this filter. This causes total to be inflated and hasMore to be incorrect when trashed tasks exist, breaking pagination.

// Filter out tasks whose linked pages are trashed
const filteredTasks = tasks.filter(task => !task.page?.isTrashed);
// Enrich tasks with drive and task list page info
const enrichedTasks = filteredTasks.map(task => {
const pageInfo = taskListToPageMap.get(task.taskListId);
return {
...task,
driveId: pageInfo?.driveId,
taskListPageId: pageInfo?.pageId,
taskListPageTitle: pageInfo?.taskListTitle,
};
});
// Get total count for pagination
const [countResult] = await db
.select({ total: count() })
.from(taskItems)
.where(whereCondition);

  1. Potential malformed URL: The link only checks if task.pageId exists before constructing the URL, but task.driveId is optional (per types.ts) and not validated. If driveId is undefined, this creates a malformed URL like /dashboard/undefined/page/123.

<Link
href={task.pageId ? `/dashboard/${task.driveId}/page/${task.pageId}` : '#'}
className="font-medium hover:underline line-clamp-1"


🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@2witstudios
Copy link
Owner Author

🟠 Important: Inconsistent Tool Name Sanitization

Files affected:

  • apps/web/src/app/api/ai/chat/route.ts (has sanitization)
  • apps/web/src/app/api/ai/global/[id]/messages/route.ts (missing sanitization)

The chat route correctly sanitizes MCP tool names for Gemini:

// chat/route.ts - Lines 579-594
if (currentProvider === 'google' && filteredTools) {
  const sanitizedTools: Record<string, unknown> = {};
  for (const [originalName, tool] of Object.entries(filteredTools)) {
    const sanitizedName = originalName.replace(/:/g, '__');
    sanitizedTools[sanitizedName] = tool;
  }
  filteredTools = sanitizedTools as any;
}

But the global assistant route is missing this:

// global/[id]/messages/route.ts - Lines 670-671
// Merge MCP tools with PageSpace tools (type assertion safe since MCP tools match AI SDK format)
finalTools = { ...finalTools, ...mcpToolsWithExecute } as typeof finalTools;
// ❌ No Gemini sanitization here

Impact: MCP tools with colons (e.g., mcp:filesystem:read_file) will fail when using Gemini in the global assistant.

Suggestion: Extract sanitization to a shared utility or apply the same logic here.

@2witstudios
Copy link
Owner Author

Review comments:

  1. Pagination totals include tasks from trashed pages (apps/web/src/app/api/tasks/route.ts around total count). The count query uses whereCondition only, while results filter out tasks whose linked page is trashed. This can overcount total/hasMore and keep load-more enabled when no visible tasks remain. Consider aligning the count with the same filtering (ideally in SQL) so pagination reflects returned items.

  2. Post-filtering after limit/offset can skip valid tasks (apps/web/src/app/api/tasks/route.ts around filteredTasks). Because limit/offset are applied before filtering out trashed-linked tasks, a page can return fewer items and those skipped slots never get replaced, so some valid tasks are never surfaced. Consider pushing the "page not trashed" predicate into the query so pagination operates on the filtered set.

@2witstudios
Copy link
Owner Author

🟡 Minor: Potential N+1 Query Pattern

File: apps/web/src/app/api/tasks/route.ts

The endpoint performs 4 sequential database queries:

  1. Get task list pages (line 98)
  2. Get task lists (line 117)
  3. Get tasks with relations (line 168)
  4. Get count for pagination (line 207)
// Query 1: Get pages
const taskListPages = await db.query.pages.findMany({ ... });

// Query 2: Get task lists  
const taskListsData = await db.query.taskLists.findMany({ ... });

// Query 3: Get tasks with relations
const tasks = await db.query.taskItems.findMany({
  with: { assignee, assigneeAgent, user, page, taskList },
});

// Query 4: Get count
const [countResult] = await db.select({ total: count() })...

Not blocking - This is fine for initial implementation. If performance issues arise with many tasks/drives, consider:

  • Using innerJoin to reduce to fewer queries
  • Caching drive membership checks
  • Running count query in parallel with main query

@2witstudios
Copy link
Owner Author

🟡 Minor: Consider Extracting Sanitization Utility

File: apps/web/src/app/api/ai/chat/route.ts (lines 579-594)

The Gemini tool name sanitization is implemented inline. Since this logic:

  1. Is also needed in the global assistant route (see issue above)
  2. May be needed in future AI routes
  3. Has specific provider-awareness logic

Consider extracting it to a shared utility:

// Suggested: apps/web/src/lib/ai/core/tool-utils.ts
export function sanitizeToolNamesForProvider(
  tools: Record<string, unknown>,
  provider: string
): Record<string, unknown> {
  // Only Gemini requires sanitization
  if (provider !== 'google') return tools;
  
  const sanitized: Record<string, unknown> = {};
  for (const [name, tool] of Object.entries(tools)) {
    sanitized[name.replace(/:/g, '__')] = tool;
  }
  return sanitized;
}

Not blocking - This is a code quality improvement that can be done later.

@2witstudios
Copy link
Owner Author

✅ Excellent: WebSocket Security Implementation

Files:

  • apps/web/src/app/api/mcp-ws/route.ts
  • apps/web/src/lib/websocket/ws-security.ts
  • apps/web/src/lib/websocket/ws-connections.ts

The WebSocket auth migration is well-implemented with defense-in-depth:

  1. JWT Validation - Initial connection auth via verifyAuth()
  2. Challenge-Response - Post-connection verification with SHA256
  3. Brute Force Protection - Max 3 attempts, 30-second expiry
  4. Fingerprint Verification - IP + User-Agent hash checked on each ping
  5. JWT Expiry Timer - Automatic disconnection when token expires
  6. Comprehensive Logging - All security events tracked

Test coverage for this is also excellent in route.security.test.ts with OWASP-aligned test cases.

Great work on this security refactor! 👏

@2witstudios
Copy link
Owner Author

Code Review Response

Thanks for the thorough review! I've addressed the feedback:

✅ TypeScript Test Fixtures (f013ee3)

Fixed the missing type properties in test mock objects by:

  • Adding createPageFixture helper with all 35 required page properties
  • Adding createTaskListFixture helper with all 10 required task list properties
  • Using as const assertions for enum-like fields (type, status, priority) to preserve literal types
  • Added missing metadata field to createTaskFixture

✅ Pagination/Trashed Page Filtering (fd5400c - already fixed)

The pagination issues were already addressed in a previous commit:

  • Trashed page IDs are now queried upfront (lines 170-173 in tasks route)
  • Filter condition includes trashed page exclusion at SQL level (lines 179-182)
  • Same whereCondition is used for both the query and count (lines 204, 242)
  • No post-filtering needed - all filtering happens in the database query

✅ Gemini Tool Name Sanitization (d039f57)

Added tool name sanitization to global assistant messages route to match the chat route:

  • Converts colons to double underscores for Gemini compatibility
  • mcp:server:toolmcp__server__tool
  • parseMCPToolName already supports both formats

Regarding N+1 Query Pattern

The 4 sequential queries in the tasks route are intentional:

  1. Task list pages query - to get accessible TASK_LIST pages
  2. Task lists query - to map task lists to pages
  3. Trashed pages query - for exclusion filter
  4. Task items query - the main data query

These cannot be easily parallelized due to dependencies between them. The queries are simple index-based lookups and the total overhead is minimal.

claude and others added 6 commits January 15, 2026 12:51
Add a Tasks button to the left sidebar that shows all tasks assigned to
the current user, with scoping similar to activity feeds:
- Dashboard level: Shows tasks across all drives
- Drive level: Shows tasks within that specific drive

Changes:
- Add Tasks button to left sidebar (next to Activity)
- Create /api/tasks endpoint for scoped task queries
- Add TasksDashboard component with filtering by status/priority/drive
- Add getDriveIdsForUser permission helper function
- Create /dashboard/tasks and /dashboard/[driveId]/tasks pages
- Fix pagination count mismatch: moved isTrashed filtering to SQL level
  so count query matches filtered results
- Fix potential malformed URL: validate driveId exists before
  constructing navigation links in TasksDashboard
- Add comprehensive tests for /api/tasks endpoint (18 test cases)
  covering auth, authorization, validation, pagination, and errors
Add createPageFixture and createTaskListFixture helpers with all required
properties to satisfy Drizzle schema types. Use 'as const' assertions for
enum-like fields (type, status, priority) to preserve literal types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add tool name sanitization for Gemini provider compatibility in the
global assistant messages route. Converts colons in MCP tool names
to double underscores (e.g., mcp:server:tool → mcp__server__tool)
since Gemini doesn't allow multiple colons in function names.

This matches the existing sanitization in the chat route.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios 2witstudios force-pushed the claude/add-tasks-sidebar-button-Sl1l5 branch from 4d7e490 to 25035ee Compare January 15, 2026 18:52
Copy link
Contributor

@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

🤖 Fix all issues with AI agents
In `@apps/web/src/app/api/tasks/route.ts`:
- Around line 228-236: The enrichment step using
taskListToPageMap.get(task.taskListId) can return undefined and produce
enrichedTasks with driveId undefined; update the logic in the enrichedTasks
creation to skip or handle orphaned tasks by filtering out tasks where pageInfo
is falsy (i.e., ensure pageInfo exists before mapping) or provide a safe
fallback (e.g., omit driveId/taskListPageId/taskListPageTitle or set a sentinel)
and optionally log a warning for missing pageInfo so frontend URLs like
/dashboard/{driveId}/page/{id} are never built with undefined values.
🧹 Nitpick comments (6)
packages/lib/src/permissions/permissions.ts (1)

10-47: Consider parallelizing independent DB queries for better performance.

The three queries (owned drives, member drives, page permissions) are independent and could execute concurrently using Promise.all. This would reduce latency, especially when the user has access through multiple sources.

♻️ Optional: Parallelize queries
 export async function getDriveIdsForUser(userId: string): Promise<string[]> {
   const driveIdSet = new Set<string>();

-  // 1. Get drives owned by user
-  const ownedDrives = await db.select({ id: drives.id })
+  // Execute all queries in parallel
+  const [ownedDrives, memberDrives, pageDrives] = await Promise.all([
+    db.select({ id: drives.id })
       .from(drives)
-      .where(eq(drives.ownerId, userId));
-
-  for (const drive of ownedDrives) {
-    driveIdSet.add(drive.id);
-  }
-
-  // 2. Get drives where user is a member
-  const memberDrives = await db.select({ driveId: driveMembers.driveId })
+      .where(eq(drives.ownerId, userId)),
+    db.select({ driveId: driveMembers.driveId })
       .from(driveMembers)
-      .where(eq(driveMembers.userId, userId));
-
-  for (const membership of memberDrives) {
-    driveIdSet.add(membership.driveId);
-  }
-
-  // 3. Get drives where user has page permissions
-  const pageDrives = await db.select({ driveId: pages.driveId })
+      .where(eq(driveMembers.userId, userId)),
+    db.select({ driveId: pages.driveId })
       .from(pagePermissions)
       .leftJoin(pages, eq(pagePermissions.pageId, pages.id))
       .where(and(
         eq(pagePermissions.userId, userId),
         eq(pagePermissions.canView, true)
-      ));
+      )),
+  ]);

+  for (const drive of ownedDrives) {
+    driveIdSet.add(drive.id);
+  }
+  for (const membership of memberDrives) {
+    driveIdSet.add(membership.driveId);
+  }
   for (const page of pageDrives) {
     if (page.driveId) {
       driveIdSet.add(page.driveId);
     }
   }

   return Array.from(driveIdSet);
 }
apps/web/src/app/api/tasks/route.ts (1)

168-173: Trashed pages query fetches globally instead of scoped to accessible drives.

This query retrieves all trashed pages in the system regardless of the user's accessible drives. For users with limited access, this is inefficient and retrieves unnecessary data. Consider scoping the query to only pages within driveIds.

♻️ Scope trashed pages query to accessible drives
     // Get trashed page IDs to exclude tasks referencing them
     // This ensures pagination counts match the actual filtered results
     const trashedPages = await db.select({ id: pages.id })
       .from(pages)
-      .where(eq(pages.isTrashed, true));
+      .where(and(
+        eq(pages.isTrashed, true),
+        inArray(pages.driveId, driveIds)
+      ));
     const trashedPageIds = trashedPages.map(p => p.id);
apps/web/src/app/api/tasks/__tests__/route.test.ts (1)

401-421: Consider adding assertions to verify filter conditions are applied.

The filter tests verify that db.query.taskItems.findMany is called but don't assert that the correct filter conditions were passed. While deep mocking is complex, you could capture and verify the where argument structure.

// Example: Capture and verify filter was applied
it('should filter by status', async () => {
  const request = new Request('https://example.com/api/tasks?context=user&status=completed');
  await GET(request);

  expect(db.query.taskItems.findMany).toHaveBeenCalledWith(
    expect.objectContaining({
      where: expect.anything(), // Could use a custom matcher
    })
  );
  // Verify eq() was called with status filter
  expect(eq).toHaveBeenCalledWith(expect.anything(), 'completed');
});
apps/web/src/components/tasks/TasksDashboard.tsx (1)

106-157: Consider using SWR for server state management.

Based on project learnings, SWR is the recommended approach for server state and caching. The current implementation uses manual fetch with useState, which works but doesn't provide automatic revalidation, caching, or deduplication benefits.

This is a nice-to-have refactor that could be addressed in a follow-up.

Based on learnings, the project prefers SWR for server state management and caching. Consider refactoring to use useSWR or useSWRInfinite for the tasks fetching, which would provide automatic revalidation and better caching behavior.

apps/web/src/app/dashboard/[driveId]/tasks/page.tsx (2)

10-11: Consider safer type handling for route params.

The as string cast is generally safe for single-segment dynamic routes like [driveId], but for stricter type safety, consider validating the param type.

🔧 Suggested improvement
  const params = useParams();
- const driveId = params.driveId as string;
+ const driveId = typeof params.driveId === 'string' ? params.driveId : params.driveId?.[0];
+ 
+ if (!driveId) {
+   return (
+     <div className="flex items-center justify-center h-full">
+       <p className="text-muted-foreground">Invalid drive ID</p>
+     </div>
+   );
+ }

20-32: Extract skeleton fallback to reduce duplication.

The loading skeleton UI is duplicated between DriveTasksContent (lines 21-31) and the Suspense fallback in DriveTasksPage (lines 55-63). Consider extracting to a shared component.

♻️ Suggested refactor
+function TasksPageSkeleton() {
+  return (
+    <div className="h-full overflow-y-auto">
+      <div className="container mx-auto px-4 py-10 sm:px-6 lg:px-10 max-w-5xl">
+        <div className="space-y-6">
+          <Skeleton className="h-8 w-48" />
+          <Skeleton className="h-10 w-full" />
+          <Skeleton className="h-96" />
+        </div>
+      </div>
+    </div>
+  );
+}

 function DriveTasksContent() {
   // ...
   if (isLoading) {
-    return (
-      <div className="h-full overflow-y-auto">
-        <div className="container mx-auto px-4 py-10 sm:px-6 lg:px-10 max-w-5xl">
-          <div className="space-y-6">
-            <Skeleton className="h-8 w-48" />
-            <Skeleton className="h-10 w-full" />
-            <Skeleton className="h-96" />
-          </div>
-        </div>
-      </div>
-    );
+    return <TasksPageSkeleton />;
   }
   // ...
 }

 export default function DriveTasksPage() {
   return (
-    <Suspense
-      fallback={
-        <div className="h-full overflow-y-auto">
-          ...
-        </div>
-      }
-    >
+    <Suspense fallback={<TasksPageSkeleton />}>
       <DriveTasksContent />
     </Suspense>
   );
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d7e490 and 25035ee.

📒 Files selected for processing (11)
  • apps/web/src/app/api/tasks/__tests__/route.test.ts
  • apps/web/src/app/api/tasks/route.ts
  • apps/web/src/app/dashboard/[driveId]/tasks/page.tsx
  • apps/web/src/app/dashboard/tasks/page.tsx
  • apps/web/src/components/layout/left-sidebar/index.tsx
  • apps/web/src/components/tasks/TasksDashboard.tsx
  • apps/web/src/components/tasks/index.ts
  • apps/web/src/components/tasks/types.ts
  • packages/lib/src/index.ts
  • packages/lib/src/permissions/index.ts
  • packages/lib/src/permissions/permissions.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/src/components/tasks/index.ts
  • packages/lib/src/permissions/index.ts
  • apps/web/src/app/dashboard/tasks/page.tsx
  • packages/lib/src/index.ts
  • apps/web/src/components/tasks/types.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/lib/src/permissions/permissions.ts
  • apps/web/src/components/tasks/TasksDashboard.tsx
  • apps/web/src/app/api/tasks/__tests__/route.test.ts
  • apps/web/src/components/layout/left-sidebar/index.tsx
  • apps/web/src/app/dashboard/[driveId]/tasks/page.tsx
  • apps/web/src/app/api/tasks/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/lib/src/permissions/permissions.ts
  • apps/web/src/app/api/tasks/__tests__/route.test.ts
  • apps/web/src/app/api/tasks/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/permissions/permissions.ts
  • apps/web/src/components/tasks/TasksDashboard.tsx
  • apps/web/src/app/api/tasks/__tests__/route.test.ts
  • apps/web/src/components/layout/left-sidebar/index.tsx
  • apps/web/src/app/dashboard/[driveId]/tasks/page.tsx
  • apps/web/src/app/api/tasks/route.ts
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

**/*.tsx: React component files should use PascalCase (e.g., UserProfile.tsx)
Use @dnd-kit for drag-and-drop functionality
Use Zustand for client state management
Use SWR for server state management and caching

Files:

  • apps/web/src/components/tasks/TasksDashboard.tsx
  • apps/web/src/components/layout/left-sidebar/index.tsx
  • apps/web/src/app/dashboard/[driveId]/tasks/page.tsx
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use Tailwind CSS and shadcn/ui components for styling and UI

Files:

  • apps/web/src/components/tasks/TasksDashboard.tsx
  • apps/web/src/components/layout/left-sidebar/index.tsx
  • apps/web/src/app/dashboard/[driveId]/tasks/page.tsx
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/components/tasks/TasksDashboard.tsx
  • apps/web/src/app/api/tasks/__tests__/route.test.ts
  • apps/web/src/components/layout/left-sidebar/index.tsx
  • apps/web/src/app/dashboard/[driveId]/tasks/page.tsx
  • apps/web/src/app/api/tasks/route.ts
apps/web/src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/components/**/*.{ts,tsx}: When document editing, register editing state with useEditingStore.getState().startEditing() to prevent UI refreshes, and clean up in return statement
When AI is streaming, register streaming state with useEditingStore.getState().startStreaming() to prevent UI refreshes, and clean up in return statement
For SWR data fetching with editing protection, use isPaused: () => hasLoadedRef.current && isEditingActive() to allow initial fetch and only pause after, with revalidateOnFocus: false
Use Zustand for client-side state management as the primary state solution
Use SWR for server state and caching with proper configuration including revalidateOnFocus: false for editing protection
Use TipTap rich text editor with markdown support for document editing
Use Monaco Editor for code editing features
Use @dnd-kit for drag-and-drop functionality instead of other libraries
Use Tailwind CSS with shadcn/ui components for all UI styling and components

Files:

  • apps/web/src/components/tasks/TasksDashboard.tsx
  • apps/web/src/components/layout/left-sidebar/index.tsx
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/tasks/route.ts
🧠 Learnings (10)
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • apps/web/src/app/api/tasks/__tests__/route.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • apps/web/src/app/api/tasks/__tests__/route.test.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : Use SWR for server state and caching with proper configuration including `revalidateOnFocus: false` for editing protection

Applied to files:

  • apps/web/src/components/layout/left-sidebar/index.tsx
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.tsx : Use SWR for server state management and caching

Applied to files:

  • apps/web/src/components/layout/left-sidebar/index.tsx
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : In Route Handlers, get search parameters with `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : In Route Handlers, get request body with `const body = await request.json();`

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : In Next.js 15 route handlers, `params` in dynamic routes are Promise objects and MUST be awaited before destructuring

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Get search parameters using `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/tasks/route.ts
🧬 Code graph analysis (3)
apps/web/src/app/api/tasks/__tests__/route.test.ts (5)
apps/web/src/lib/auth/index.ts (3)
  • WebAuthResult (26-29)
  • AuthError (33-35)
  • authenticateRequestWithOptions (221-291)
packages/db/src/index.ts (1)
  • db (20-20)
apps/web/src/app/api/tasks/route.ts (1)
  • GET (31-262)
apps/processor/src/logger.ts (1)
  • error (57-63)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
apps/web/src/app/dashboard/[driveId]/tasks/page.tsx (3)
apps/web/src/hooks/useDrive.ts (1)
  • useDriveStore (21-74)
apps/web/src/components/ui/skeleton.tsx (1)
  • Skeleton (145-145)
apps/web/src/components/tasks/TasksDashboard.tsx (1)
  • TasksDashboard (43-410)
apps/web/src/app/api/tasks/route.ts (3)
apps/web/src/lib/auth/index.ts (1)
  • authenticateRequestWithOptions (221-291)
packages/lib/src/permissions/index.ts (2)
  • isUserDriveMember (15-15)
  • getDriveIdsForUser (11-11)
packages/db/src/schema/tasks.ts (2)
  • taskLists (11-28)
  • taskItems (40-65)
🔇 Additional comments (17)
apps/web/src/components/layout/left-sidebar/index.tsx (1)

152-158: LGTM!

The Tasks navigation item follows the same conditional URL pattern as the Activity link above, maintaining consistency in the sidebar navigation. The styling and structure are consistent with the other sidebar items.

apps/web/src/app/api/tasks/route.ts (3)

1-6: LGTM on imports and authentication setup.

The imports follow project conventions, authentication options correctly allow jwt/mcp without CSRF (appropriate for a read-only GET endpoint), and the centralized permission functions from @pagespace/lib are used as per coding guidelines.


37-51: Good use of URL search params extraction.

The search parameters extraction follows the coding guidelines using new URL(request.url) and handles nullable params correctly with the nullish coalescing operator.


62-109: Drive access authorization is properly implemented.

The authorization flow correctly validates drive access for both contexts:

  • Drive context: Uses isUserDriveMember to verify access
  • User context: Uses getDriveIdsForUser and validates any optional driveId filter against accessible drives

Returns appropriate 403 responses for unauthorized access and handles empty drive lists gracefully.

apps/web/src/app/api/tasks/__tests__/route.test.ts (5)

1-5: LGTM on test setup and imports.

The eslint-disable for @typescript-eslint/no-explicit-any is acceptable in test files where mocking often requires type flexibility. The imports are correctly structured with mocks defined before usage.


85-180: Well-structured test fixtures with proper TypeScript typing.

The fixture helpers (createPageFixture, createTaskListFixture, createTaskFixture) use Partial<> for overrides and provide sensible defaults. This makes tests readable and maintainable.


209-231: Good authentication test coverage.

Tests correctly verify 401 on auth failure and validate that authenticateRequestWithOptions is called with the expected auth options.


233-274: Authorization tests cover key scenarios.

Tests cover 403 for inaccessible drives in both drive and user contexts, and verify that getDriveIdsForUser is called with the correct user ID.


448-469: Error handling tests are comprehensive.

Tests verify both the 500 response and that the error is logged via loggers.api.error. Good coverage of the error path.

apps/web/src/components/tasks/TasksDashboard.tsx (6)

1-22: LGTM on imports and type usage.

The component correctly uses 'use client' directive, imports follow project conventions, and types are properly imported from the local ./types module.


43-61: State initialization from URL search params is well implemented.

The component correctly initializes filter state from URL search params with proper type casting. The pattern of using a function initializer for useState prevents unnecessary recomputation.


88-104: Intentional eslint-disable for exhaustive-deps is acceptable here.

The comment explains that selectedDriveId is only checked, not derived from, which justifies excluding it from the dependency array. This prevents an infinite loop where setting selectedDriveId would trigger a refetch which would set it again.


454-459: URL construction correctly guards against undefined values.

The Link href condition task.pageId && task.driveId properly prevents malformed URLs when either value is undefined. This addresses the PR comment concern about URLs like /dashboard/undefined/page/123.


465-476: Task list link also has proper guards.

The condition task.taskListPageTitle && task.driveId && task.taskListPageId ensures all required values are present before rendering the link. Good defensive coding.


487-503: Date handling is correct and user-friendly.

The due date display logic properly handles overdue, due today, and future due dates with appropriate styling. Using date-fns functions (isPast, isToday, format, formatDistanceToNow) is idiomatic.

apps/web/src/app/dashboard/[driveId]/tasks/page.tsx (2)

1-8: LGTM!

Imports are correctly structured with proper module sources. The 'use client' directive is appropriately placed for this interactive component.


51-69: LGTM!

The Suspense boundary provides a good pattern for consistency and future-proofing, even though the current client-side data fetching via useDriveStore won't suspend. The actual loading state is correctly handled by the isLoading check in DriveTasksContent.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Skip tasks where taskListToPageMap lookup returns undefined to prevent
enrichedTasks having driveId: undefined. This ensures frontend URLs
like /dashboard/{driveId}/page/{id} are never built with undefined values.

Added warning log for orphaned tasks to help identify data integrity issues.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios 2witstudios merged commit 0e241eb into master Jan 15, 2026
3 checks passed
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.

3 participants