Skip to content

feat: Version History & Rollback - #118

Merged
2witstudios merged 37 commits into
masterfrom
feature/version-history-rollback
Dec 23, 2025
Merged

2witstudios merged 37 commits into
masterfrom
feature/version-history-rollback

Conversation

@2witstudios

@2witstudios 2witstudios commented Dec 22, 2025 •

Copy link
Copy Markdown
Owner

Summary

Add comprehensive version history browsing and rollback capabilities for PageSpace, allowing users to restore resources to previous states based on the existing activity logging infrastructure.

Key Features

  • Version History Browser: Slide-out panel showing chronological changes with filters (operation type, AI-only)
  • RBAC-Based Rollback: If you can edit something, you can rollback changes to it
  • Plan-Based Retention: Free=7d, Pro=30d, Business=90d, Founder=unlimited
  • Non-Destructive: Rollback creates a new activity entry (history is never erased)

Changes

Schema (packages/db/)

  • Added rollback operation to activity enum
  • Added rollbackFromActivityId, contentFormat fields to activity_logs
  • Created retention_policies table

Backend (packages/lib/, apps/web/src/services/)

  • Rollback permissions module with context-aware checks
  • Rollback service with resource-specific handlers
  • logRollbackActivity() function

API Endpoints (apps/web/src/app/api/)

  • GET /api/activities/[activityId] - Single activity with rollback eligibility
  • POST /api/activities/[activityId]/rollback - Execute rollback
  • GET /api/pages/[pageId]/history - Page version history
  • GET /api/drives/[driveId]/history - Drive version history (admin)

UI Components (apps/web/src/components/version-history/)

  • VersionHistoryPanel - Main slide-out panel
  • VersionHistoryItem - Activity item with "Restore" action
  • RollbackConfirmDialog - Confirmation modal

Test plan

  • Run pnpm db:migrate to apply schema changes
  • Verify version history loads for a page (GET /api/pages/{id}/history)
  • Test rollback flow: make change → view history → restore → verify state
  • Verify RBAC: non-editors cannot rollback
  • Test retention filtering based on user plan

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added AI-driven undo for chat messages with preview and execute modes to assess and apply rollbacks.
  • Bug Fixes

    • Standardized undo failure responses to a single error message and consistent 500 status for failures.
  • Tests

    • Consolidated and extended tests for undo behavior and added comprehensive rollback-permissions contract tests; updated related hook tests.

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

Implement comprehensive version history browsing and rollback capabilities
for PageSpace, allowing users to restore resources to previous states.

## Schema Changes
- Add 'rollback' operation to activity_operation enum
- Add rollbackFromActivityId and contentFormat fields to activity_logs
- Create retention_policies table for plan-based history retention

## Core Features
- RBAC-based rollback permissions (edit access = rollback access)
- Resource-specific rollback handlers (pages, drives, agents, etc.)
- Plan-based retention limits (7/30/90/unlimited days)
- Rollback creates new activity entry (history never erased)

## API Endpoints
- GET /api/activities/[activityId] - Single activity with rollback eligibility
- POST /api/activities/[activityId]/rollback - Execute rollback
- GET /api/pages/[pageId]/history - Page version history
- GET /api/drives/[driveId]/history - Drive version history (admin)

## UI Components
- VersionHistoryPanel - Slide-out panel with timeline and filters
- VersionHistoryItem - Activity item with "Restore" action
- RollbackConfirmDialog - Confirmation modal with warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

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

coderabbitai Bot commented Dec 22, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds an AI-driven undo service with preview and execute flows, standardizes undo failure responses to always return HTTP 500 with a single error message, expands rollback-permissions tests, and updates several component/hook tests and a header prop usage.

Changes

Cohort / File(s) Change Summary
AI Undo Service
apps/web/src/services/api/ai-undo-service.ts
New service implementing AI-driven undo preview and execute flows; types added (AiUndoPreview, UndoMode, AiUndoResult); exports previewAiUndo and executeAiUndo; integrates rollback preview/execute and transactional execution with logging.
Undo API Endpoint & Tests
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts, apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
Failure branch standardized: all failures return 500 with message "Undo failed. No changes were applied."; tests updated to assert uniform failure payload and status; removed/merged partial-failure expectations.
Rollback Permissions Tests
packages/lib/src/permissions/__tests__/rollback-permissions.test.ts
Added comprehensive contract tests for rollback permission logic and helper pure functions across multiple contexts with mocked peripheral permission checks.
Component Refactor
apps/web/src/components/layout/middle-content/CenterPanel.tsx
OptimizedViewHeader no longer uses useParams; now derives behavior from pathname and pageId prop.
Hook Tests — Breadcrumbs
apps/web/src/hooks/__tests__/useBreadcrumbs.test.ts
Test updated: null pageId now expects isLoading false (no fetch) and test name/comments adjusted accordingly.
Hook Tests — Permissions
apps/web/src/hooks/__tests__/usePermissions.test.ts
SWR mocks replaced with mockImplementation that invokes onSuccess to simulate initial load; tests updated to reflect loaded permission state and isPaused expectations.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as Undo API Endpoint
    participant Service as AI Undo Service
    participant Rollback as Rollback Service
    participant DB as Database

    rect rgb(235,245,255)
    Note over Client,Service: Preview flow
    Client->>API: POST /api/ai/chat/messages/[messageId]/undo (preview)
    API->>Service: previewAiUndo(messageId, userId)
    Service->>DB: fetch target message & subsequent messages
    Service->>Service: derive drive/context & collect activities
    Service->>Rollback: previewRollback(activities)
    Rollback-->>Service: feasibility per activity
    Service-->>API: AiUndoPreview (affected messages, rollback feasibility)
    API-->>Client: 200 OK + preview
    end

    rect rgb(235,255,235)
    Note over Client,Service: Execute flow
    Client->>API: POST /api/ai/chat/messages/[messageId]/undo (execute)
    API->>Service: executeAiUndo(messageId, userId, mode)
    Service->>DB: BEGIN TRANSACTION
    alt mode == messages_and_changes
        Service->>Rollback: executeRollback(activities) in reverse order
        Rollback->>DB: apply rollback ops
        Rollback-->>Service: results
    end
    Service->>DB: soft-delete messages from target forward
    Service->>DB: COMMIT
    Service-->>API: AiUndoResult (success)
    API-->>Client: 200 OK + result
    end

    rect rgb(255,235,235)
    Note over Client,Service: Error path
    Service->>DB: operation fails
    Service->>DB: ROLLBACK
    Service-->>API: AiUndoResult (success:false, errors)
    API-->>Client: 500 Internal Server Error ("Undo failed. No changes were applied.")
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Version History & Rollback' directly and clearly summarizes the main changes across the PR, which introduce version history browsing and rollback capabilities throughout the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/version-history-rollback

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (6)
apps/web/src/app/api/pages/[pageId]/history/route.ts (1)

92-97: Rollback eligibility logic is consistent.

The canRollback computation matches the drive history endpoint, checking both operation type and presence of restorable state. This consistency is good for maintainability.

Consider extracting this logic into a shared helper to ensure it stays synchronized.

apps/web/src/components/version-history/RollbackConfirmDialog.tsx (2)

28-37: activityId prop is declared but unused.

The activityId prop is destructured but never referenced in the component. If it's intended for future use or logging, consider removing it to avoid confusion, or add a comment explaining its purpose.

🔎 If not needed, remove from props
 export function RollbackConfirmDialog({
   open,
   onOpenChange,
-  activityId,
   resourceTitle,
   operation,
   timestamp,
   warnings,
   onConfirm,
 }: RollbackConfirmDialogProps) {

And update the interface:

 interface RollbackConfirmDialogProps {
   open: boolean;
   onOpenChange: (open: boolean) => void;
-  activityId: string;
   resourceTitle: string | null;

40-50: Error handling only logs to console — consider user feedback.

When onConfirm throws, the error is logged but the user receives no notification. The dialog remains open due to the error, which is good, but a toast or inline error message would improve UX.

🔎 Example with toast notification
+'use client';
+
+import { useState } from 'react';
+import { AlertTriangle, History, Loader2 } from 'lucide-react';
+import { toast } from 'sonner'; // or your toast library
 // ... other imports

   const handleConfirm = async () => {
     setIsLoading(true);
     try {
       await onConfirm();
       onOpenChange(false);
     } catch (error) {
       console.error('Rollback failed:', error);
+      toast.error('Failed to restore version. Please try again.');
     } finally {
       setIsLoading(false);
     }
   };
apps/web/src/components/version-history/VersionHistoryPanel.tsx (1)

127-155: Consider removing error re-throw or document the intent.

The handleRollback function displays an error toast and then re-throws the error (line 153). This pattern is unusual because:

  1. The error is already handled and displayed to the user via toast
  2. Re-throwing causes the error to propagate, but there's no try-catch wrapper in the calling code
  3. This will result in an unhandled promise rejection

If the intent is to prevent the RollbackConfirmDialog from closing on error, this should be documented. Otherwise, consider removing the throw statement.

🔎 Proposed fix
     } catch (error) {
       toast({
         title: 'Error',
         description: error instanceof Error ? error.message : 'Failed to rollback',
         variant: 'destructive',
       });
-      throw error;
     }
   };
apps/web/src/services/api/rollback-service.ts (2)

361-403: Consider wrapping database updates in transactions for atomicity.

The rollbackPageChange function updates the page table directly without using a transaction (lines 393-400). While this may work for simple cases, consider:

  1. If multiple fields are being restored and the update fails partway through, data could be inconsistent
  2. The activity logging happens after the update, so they're not atomic
  3. For complex rollbacks (especially in other handlers), transactions would provide better guarantees
🔎 Example transaction pattern
await db.transaction(async (tx) => {
  await tx
    .update(pages)
    .set({
      ...updateData,
      updatedAt: new Date(),
    })
    .where(eq(pages.id, activity.pageId));
  
  // Any related updates would go here
});

Note: The activity logging in executeRollback would still be outside the transaction, which is likely fine for audit purposes.


226-238: Consider using deep equality comparison instead of JSON.stringify.

The change detection logic uses JSON.stringify for comparison (lines 228-229), which can produce false positives due to:

  1. Key ordering differences in objects
  2. Date serialization variations
  3. undefined vs null treatment
  4. Floating-point precision

Since this only affects warnings shown to users, it's not critical, but consider using a proper deep equality function (e.g., from lodash or a similar library) for more accurate detection.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e53895 and c796ac9.

📒 Files selected for processing (18)
  • apps/web/src/app/api/activities/[activityId]/rollback/route.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/components/activity/constants.ts
  • apps/web/src/components/version-history/RollbackConfirmDialog.tsx
  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • apps/web/src/components/version-history/index.ts
  • apps/web/src/services/api/index.ts
  • apps/web/src/services/api/rollback-service.ts
  • packages/db/drizzle/0025_icy_wallflower.sql
  • packages/db/drizzle/meta/0025_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema/monitoring.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • packages/lib/src/permissions/index.ts
  • packages/lib/src/permissions/rollback-permissions.ts
🧰 Additional context used
📓 Path-based instructions (10)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/activities/[activityId]/rollback/route.ts
  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{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

Files:

  • apps/web/src/app/api/activities/[activityId]/rollback/route.ts
  • apps/web/src/components/version-history/index.ts
  • packages/lib/src/permissions/index.ts
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/components/activity/constants.ts
  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/services/api/rollback-service.ts
  • apps/web/src/services/api/index.ts
  • packages/lib/src/permissions/rollback-permissions.ts
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
  • apps/web/src/components/version-history/RollbackConfirmDialog.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/activities/[activityId]/rollback/route.ts
  • apps/web/src/components/version-history/index.ts
  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/components/activity/constants.ts
  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/services/api/rollback-service.ts
  • apps/web/src/services/api/index.ts
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
  • apps/web/src/components/version-history/RollbackConfirmDialog.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);

Files:

  • apps/web/src/app/api/activities/[activityId]/rollback/route.ts
  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/app/api/drives/[driveId]/history/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/activities/[activityId]/rollback/route.ts
  • apps/web/src/components/version-history/index.ts
  • packages/lib/src/permissions/index.ts
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/components/activity/constants.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/services/api/rollback-service.ts
  • apps/web/src/services/api/index.ts
  • packages/lib/src/permissions/rollback-permissions.ts
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/activities/[activityId]/rollback/route.ts
  • apps/web/src/components/version-history/index.ts
  • packages/db/drizzle/meta/_journal.json
  • packages/lib/src/permissions/index.ts
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/components/activity/constants.ts
  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/services/api/rollback-service.ts
  • apps/web/src/services/api/index.ts
  • packages/lib/src/permissions/rollback-permissions.ts
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
  • apps/web/src/components/version-history/RollbackConfirmDialog.tsx
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for database queries with PostgreSQL

Files:

  • packages/db/src/schema/monitoring.ts
apps/web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.tsx: For document editing, register editing state using useEditingStore.getState().startEditing() and endEditing() to prevent unwanted UI refreshes
For AI streaming operations, register streaming state using useEditingStore.getState().startStreaming() and endStreaming() to prevent unwanted UI refreshes
When using SWR, check useEditingStore state with isAnyActive() and set isPaused to prevent data refreshes during editing or streaming

Files:

  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • apps/web/src/components/version-history/RollbackConfirmDialog.tsx
**/*.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/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • apps/web/src/components/version-history/RollbackConfirmDialog.tsx
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • apps/web/src/components/version-history/RollbackConfirmDialog.tsx
🧠 Learnings (8)
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • packages/lib/src/permissions/index.ts
  • packages/lib/src/permissions/rollback-permissions.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/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • packages/lib/src/permissions/index.ts
  • packages/lib/src/permissions/rollback-permissions.ts
📚 Learning: 2025-12-14T14:54:15.319Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.319Z
Learning: Applies to apps/**/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` for access control, such as `getUserAccessLevel()` and `canUserEditPage()`

Applied to files:

  • packages/lib/src/permissions/index.ts
  • packages/lib/src/permissions/rollback-permissions.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • packages/lib/src/permissions/index.ts
  • packages/lib/src/permissions/rollback-permissions.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
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/pages/[pageId]/history/route.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
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/activities/[activityId]/route.ts
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/web/app/**/{route,route.ts,route.js} : In Next.js 15, `params` in dynamic routes are Promise objects. You MUST await `context.params` before destructuring.

Applied to files:

  • apps/web/src/app/api/activities/[activityId]/route.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Get request body using `const body = await request.json();`

Applied to files:

  • apps/web/src/app/api/activities/[activityId]/route.ts
🧬 Code graph analysis (5)
apps/web/src/components/version-history/VersionHistoryItem.tsx (8)
apps/web/src/components/activity/types.ts (1)
  • ActivityLog (10-31)
apps/web/src/components/version-history/index.ts (2)
  • VersionHistoryItem (2-2)
  • RollbackConfirmDialog (3-3)
apps/web/src/components/activity/constants.ts (3)
  • operationConfig (17-30)
  • defaultOperationConfig (39-43)
  • resourceTypeIcons (32-37)
apps/web/src/components/activity/utils.ts (1)
  • getInitials (4-16)
apps/web/src/components/ui/badge.tsx (1)
  • Badge (46-46)
apps/web/src/components/ui/dropdown-menu.tsx (4)
  • DropdownMenu (242-242)
  • DropdownMenuTrigger (244-244)
  • DropdownMenuContent (245-245)
  • DropdownMenuItem (248-248)
apps/web/src/components/ui/button.tsx (1)
  • Button (59-59)
apps/web/src/components/version-history/RollbackConfirmDialog.tsx (1)
  • RollbackConfirmDialog (28-113)
apps/web/src/components/version-history/VersionHistoryPanel.tsx (2)
apps/web/src/components/activity/types.ts (1)
  • ActivityLog (10-31)
apps/web/src/components/version-history/VersionHistoryItem.tsx (1)
  • VersionHistoryItem (26-146)
apps/web/src/app/api/activities/[activityId]/route.ts (3)
apps/web/src/lib/auth/index.ts (2)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
packages/lib/src/permissions/rollback-permissions.ts (1)
  • RollbackContext (15-19)
apps/web/src/services/api/rollback-service.ts (2)
  • getActivityById (87-130)
  • previewRollback (135-255)
packages/lib/src/permissions/rollback-permissions.ts (2)
packages/lib/src/monitoring/activity-logger.ts (1)
  • ActivityResourceType (88-100)
packages/lib/src/permissions/index.ts (1)
  • isDriveOwnerOrAdmin (13-13)
apps/web/src/components/version-history/RollbackConfirmDialog.tsx (3)
apps/web/src/components/version-history/index.ts (1)
  • RollbackConfirmDialog (3-3)
apps/processor/src/logger.ts (1)
  • error (57-63)
apps/web/src/components/ui/badge.tsx (1)
  • Badge (46-46)
🪛 Biome (2.1.2)
packages/lib/src/permissions/rollback-permissions.ts

[error] 99-99: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 126-126: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (31)
packages/db/drizzle/meta/_journal.json (1)

180-186: LGTM!

The migration journal entry is properly structured and follows Drizzle's standard format. The index increments correctly from the previous entry.

packages/lib/src/monitoring/activity-logger.ts (1)

84-86: LGTM!

The addition of the rollback operation to the ActivityOperation type is correctly structured and properly categorized with a descriptive comment.

packages/lib/src/permissions/rollback-permissions.ts (3)

15-41: LGTM!

The type definitions are well-structured and properly documented. The RollbackContext covers all expected contexts, and the interfaces correctly model nullable fields to match the database schema.


57-72: LGTM!

The guards against non-rollbackable operations are well-reasoned. Preventing rollback of create, signup, login, and logout operations makes sense since these don't have previous state to restore. The prevention of rollback chains is also appropriate.


146-182: LGTM!

The helper functions correctly identify rollbackable resource types and operations. The inclusion of delete in the rollbackable operations list is appropriate, as delete operations can store the deleted resource's state in previousValues, enabling restoration.

packages/lib/src/permissions/index.ts (1)

19-20: LGTM!

The export statement follows the existing pattern and correctly exposes the rollback permission utilities through the centralized permissions module.

apps/web/src/components/version-history/index.ts (1)

1-3: LGTM!

The barrel export file follows standard patterns and simplifies component imports for consumers.

packages/db/drizzle/0025_icy_wallflower.sql (2)

1-1: LGTM!

The addition of the rollback value to the activity_operation enum is correctly formatted and aligns with the TypeScript type definition.


11-12: Schema columns added correctly, but verify integration with logActivity.

The new columns contentFormat and rollbackFromActivityId are properly defined as nullable text fields, which supports backward compatibility. However, as noted in the review of activity-logger.ts, these columns may not be properly populated by the logActivity function, which could result in these values being stored in metadata instead of the dedicated columns.

Ensure the ActivityLogInput interface and logActivity function are updated to support these new fields directly.

apps/web/src/components/activity/constants.ts (2)

6-6: LGTM!

The History icon import is properly added and alphabetically ordered with other lucide-react imports.


29-29: LGTM!

The rollback operation configuration is well-defined with an appropriate icon, clear label, and consistent variant choice that matches similar restoration operations.

apps/web/src/app/api/activities/[activityId]/rollback/route.ts (5)

1-12: LGTM!

The imports are correctly structured, authentication options appropriately require CSRF protection for this state-changing endpoint, and the Zod v4 schema properly validates the request body with sensible defaults.


19-29: LGTM!

The function signature correctly handles Next.js 15's async params by awaiting context.params before destructuring, as required by the coding guidelines. Authentication is properly enforced.


31-50: LGTM!

The request body parsing and validation follow best practices with proper error handling. JSON parsing is safely wrapped in try-catch, and Zod's safeParse is used for non-throwing validation with clear error messages.


52-59: LGTM!

The dry-run preview logic correctly returns early without mutating state, providing a safe preview mechanism. The response properly indicates the dry-run status.


61-77: LGTM!

The rollback execution logic properly handles both success and failure cases with appropriate status codes and comprehensive response data including warnings and restored values.

apps/web/src/app/api/drives/[driveId]/history/route.ts (2)

26-35: LGTM on Next.js 15 params handling.

The route correctly types context.params as Promise<{ driveId: string }> and awaits it before destructuring. This follows the Next.js 15 requirement for async route params. Based on learnings, this is the correct pattern.


93-97: Rollback eligibility logic is sound.

The canRollback computation correctly checks both that the operation is rollbackable AND that there's recoverable state (previousValues or contentSnapshot). This aligns with the preview logic in rollback-service.ts.

packages/db/src/schema/monitoring.ts (3)

390-393: Addition of 'rollback' operation to enum is appropriate.

The new rollback operation type enables tracking of rollback activities in the audit trail, supporting the non-destructive rollback approach described in the PR objectives.


444-449: New rollback-related fields are well-designed.

The fields support the rollback feature:

  • contentSnapshot: Stores full content state for restoration
  • contentFormat: Enables proper parsing during rollback (text/html/json/tiptap)
  • rollbackFromActivityId: Links rollback entries to their source activity

All fields are nullable, making this migration safe for existing data.


485-495: Retention policies table design is clean.

The table correctly models plan-based retention with:

  • Unique constraint on subscriptionTier for fast lookups
  • Integer retentionDays with -1 convention for unlimited (matches PR spec: Free=7d, Pro=30d, Business=90d, Founder=unlimited)
  • Proper timestamp tracking with $onUpdate for updatedAt
apps/web/src/app/api/activities/[activityId]/route.ts (2)

18-27: LGTM on Next.js 15 route handler pattern.

The handler correctly awaits context.params before destructuring activityId, following Next.js 15 requirements. Based on learnings, this is the correct approach.


57-62: Response structure is well-organized.

The response provides all necessary information for the UI to display rollback eligibility and reasons, enabling the VersionHistoryItem component to show appropriate actions.

apps/web/src/app/api/pages/[pageId]/history/route.ts (2)

25-35: LGTM on Next.js 15 route handler and authorization.

The handler correctly:

  1. Awaits context.params before destructuring (Next.js 15 requirement)
  2. Uses centralized canUserViewPage permission function (per coding guidelines)

Based on learnings and coding guidelines, this follows the required patterns.


67-79: Retention logic is consistent across history endpoints.

The retention cutoff calculation mirrors the drive history endpoint, ensuring consistent behavior. The logic correctly handles:

  • Unlimited retention (retentionDays <= 0)
  • User-provided startDate that's more recent than retention cutoff
  • Automatic enforcement of retention window
apps/web/src/components/version-history/RollbackConfirmDialog.tsx (1)

52-112: Clean UI implementation with good accessibility.

The dialog properly:

  • Disables interactive elements during loading
  • Provides clear visual feedback with the spinner
  • Supports dark mode with appropriate color classes
  • Uses semantic AlertDialog components
  • Displays warnings prominently when present
apps/web/src/services/api/index.ts (1)

44-57: LGTM!

The rollback service exports follow the established pattern in this barrel export file. Both functions and types are properly exported for consumption by API routes and UI components.

apps/web/src/services/api/rollback-service.ts (2)

278-356: Good integration with activity logging and error handling.

The executeRollback function properly:

  • Gets actor info for audit trail (line 280)
  • Delegates to resource-specific handlers
  • Logs the rollback activity with full context (lines 319-335)
  • Handles errors gracefully with appropriate logging

The use of optional chaining to convert null to undefined (e.g., line 325, 327) ensures type safety with the logging API.


700-729: Query retention_policies table instead of using hardcoded defaults.

The getUserRetentionDays function uses hardcoded tier-to-retention mappings (lines 702-707) instead of querying the retention_policies table that exists in the schema. Update the function to fetch retention values from the database, allowing policies to be configured without code changes.

apps/web/src/components/version-history/VersionHistoryPanel.tsx (1)

66-67: No action needed. The context value 'user_dashboard' is a valid RollbackContext type as defined in packages/lib/src/permissions/rollback-permissions.ts and is properly handled throughout the codebase for user dashboard activity tracking.

Likely an incorrect or invalid review comment.

apps/web/src/components/version-history/VersionHistoryItem.tsx (1)

20-24: No issues found. The context prop values in VersionHistoryItemProps align perfectly with the RollbackContext type definition. All four values ('page', 'drive', 'ai_tool', 'user_dashboard') are properly defined in the rollback permissions module and have complete permission logic implemented for each context.

Comment thread apps/web/src/app/api/activities/[activityId]/route.ts
Comment thread apps/web/src/app/api/drives/[driveId]/history/route.ts
Comment thread apps/web/src/components/version-history/VersionHistoryItem.tsx
Comment thread apps/web/src/components/version-history/VersionHistoryPanel.tsx
Comment thread apps/web/src/services/api/rollback-service.ts
Comment thread apps/web/src/services/api/rollback-service.ts Outdated
Comment thread apps/web/src/services/api/rollback-service.ts
Comment thread packages/db/drizzle/0025_icy_wallflower.sql
Comment thread packages/lib/src/monitoring/activity-logger.ts
Comment thread packages/lib/src/permissions/rollback-permissions.ts
@2witstudios

Copy link
Copy Markdown
Owner Author

Code review

Found 1 issue:

  1. Missing CSRF token in POST request - the rollback endpoint requires CSRF protection (requireCSRF: true), but the fetch call does not include the X-CSRF-Token header. This will cause all rollback requests to fail with 403 error. Other POST requests in the codebase use fetchWithAuth() from @/lib/auth/auth-fetch which automatically injects CSRF tokens.

try {
const response = await fetch(`/api/activities/${activityId}/rollback`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ context }),
});

🤖 Generated with Claude Code

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

- Fully implement rollbackPermissionChange for grant/revoke/update operations
- Fully implement rollbackMemberChange for add/remove/role change operations
- Fully implement rollbackRoleChange for create/delete/update operations
- Fix lint errors: remove unused imports, fix useEffect dependencies
- Add package exports for @pagespace/lib/permissions and @pagespace/lib/monitoring
- Fix useToast import path to use correct hook location

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
apps/web/src/services/api/rollback-service.ts (2)

818-822: Optimize count query using SQL COUNT() aggregate.

The count query selects all activity log IDs and counts the array length. This loads all matching rows into memory, which is inefficient for large activity logs.

🔎 Proposed fix
+    import { sql } from '@pagespace/db';
+    
     const [activities, countResult] = await Promise.all([
       db
         .select()
         .from(activityLogs)
         .where(and(...conditions))
         .orderBy(desc(activityLogs.timestamp))
         .limit(limit)
         .offset(offset),
       db
-        .select({ count: activityLogs.id })
+        .select({ count: sql<number>`count(*)` })
         .from(activityLogs)
         .where(and(...conditions)),
     ]);

     return {
       activities: activities.map((a) => ({
         // ... mapping
       })),
-      total: countResult.length,
+      total: countResult[0]?.count ?? 0,
     };

Apply the same fix to lines 892-894 in getDriveVersionHistory.


802-805: Validate operation parameter before type assertion to prevent query errors.

The operation parameter from user input is directly cast to an enum type without validation. Invalid operation values could cause runtime errors or unexpected query behavior.

🔎 Proposed fix
+    const validOperations = ['create', 'update', 'delete', 'restore', 'reorder', 'trash', 'move', 'permission_grant', 'permission_update', 'permission_revoke', 'agent_config_update', 'rollback'];
+    
     if (operation) {
+      if (!validOperations.includes(operation)) {
+        loggers.api.warn('[RollbackService] Invalid operation filter', { operation });
+      } else {
-      // Type assertion needed since operation comes from user input
-      conditions.push(eq(activityLogs.operation, operation as typeof activityLogs.operation.enumValues[number]));
+        conditions.push(eq(activityLogs.operation, operation as typeof activityLogs.operation.enumValues[number]));
+      }
     }

Apply the same fix to line 880 in getDriveVersionHistory.

apps/web/src/components/version-history/VersionHistoryItem.tsx (1)

41-51: Improve error handling and response validation for preview fetch.

The preview fetch has several issues:

  1. No response.ok check before parsing JSON - will throw on non-OK responses
  2. Silently catches all errors - users don't know if preview failed
  3. Uses plain fetch() instead of fetchWithAuth() for authentication
🔎 Proposed improvements
+import { fetchWithAuth } from '@/lib/auth/auth-fetch';
+
   const handleRestoreClick = async () => {
     // Fetch preview to get warnings
     try {
-      const response = await fetch(`/api/activities/${activity.id}?context=${context}`);
+      const response = await fetchWithAuth(`/api/activities/${activity.id}?context=${context}`);
+      
+      if (!response.ok) {
+        throw new Error('Failed to fetch rollback preview');
+      }
+      
       const data = await response.json();
       setPreviewWarnings(data.warnings || []);
-    } catch {
+    } catch (error) {
+      console.error('Failed to fetch rollback preview:', error);
       setPreviewWarnings([]);
+      // Note: still show dialog so user can proceed, but warnings may be incomplete
     }
     setShowConfirm(true);
   };
🧹 Nitpick comments (1)
apps/web/src/services/api/rollback-service.ts (1)

8-8: Consider splitting long import for better readability.

The import statement from @pagespace/db is very long and includes many named imports, making it harder to read and maintain.

🔎 Suggested improvement
-import { db, activityLogs, pages, drives, driveMembers, driveRoles, pagePermissions, users, eq, and, desc, gte, lte } from '@pagespace/db';
+import {
+  db,
+  activityLogs,
+  pages,
+  drives,
+  driveMembers,
+  driveRoles,
+  pagePermissions,
+  users,
+  eq,
+  and,
+  desc,
+  gte,
+  lte,
+} from '@pagespace/db';
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c796ac9 and 114772b.

📒 Files selected for processing (5)
  • apps/web/src/components/version-history/RollbackConfirmDialog.tsx
  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • apps/web/src/services/api/rollback-service.ts
  • packages/lib/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/components/version-history/RollbackConfirmDialog.tsx
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/package.json
  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/services/api/rollback-service.ts
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{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

Files:

  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/services/api/rollback-service.ts
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/services/api/rollback-service.ts
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
apps/web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.tsx: For document editing, register editing state using useEditingStore.getState().startEditing() and endEditing() to prevent unwanted UI refreshes
For AI streaming operations, register streaming state using useEditingStore.getState().startStreaming() and endStreaming() to prevent unwanted UI refreshes
When using SWR, check useEditingStore state with isAnyActive() and set isPaused to prevent data refreshes during editing or streaming

Files:

  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
**/*.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/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
**/*.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/services/api/rollback-service.ts
🧠 Learnings (6)
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • packages/lib/package.json
📚 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/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • packages/lib/package.json
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Use ESM modules throughout the codebase

Applied to files:

  • packages/lib/package.json
📚 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: Use TypeScript strict mode and ESM modules throughout the codebase

Applied to files:

  • packages/lib/package.json
📚 Learning: 2025-12-14T14:54:15.319Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.319Z
Learning: Applies to apps/**/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` for access control, such as `getUserAccessLevel()` and `canUserEditPage()`

Applied to files:

  • packages/lib/package.json
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/services/api/rollback-service.ts
🧬 Code graph analysis (2)
apps/web/src/components/version-history/VersionHistoryItem.tsx (6)
apps/web/src/components/activity/types.ts (1)
  • ActivityLog (10-31)
apps/web/src/components/version-history/index.ts (2)
  • VersionHistoryItem (2-2)
  • RollbackConfirmDialog (3-3)
apps/web/src/components/activity/constants.ts (3)
  • operationConfig (17-30)
  • defaultOperationConfig (39-43)
  • resourceTypeIcons (32-37)
apps/web/src/components/activity/utils.ts (1)
  • getInitials (4-16)
apps/web/src/components/ui/dropdown-menu.tsx (4)
  • DropdownMenu (242-242)
  • DropdownMenuTrigger (244-244)
  • DropdownMenuContent (245-245)
  • DropdownMenuItem (248-248)
apps/web/src/components/version-history/RollbackConfirmDialog.tsx (1)
  • RollbackConfirmDialog (27-111)
apps/web/src/components/version-history/VersionHistoryPanel.tsx (4)
apps/web/src/components/activity/types.ts (1)
  • ActivityLog (10-31)
apps/web/src/hooks/useToast.ts (1)
  • useToast (13-43)
apps/web/src/lib/auth/auth-fetch.ts (1)
  • fetch (44-211)
apps/web/src/components/version-history/VersionHistoryItem.tsx (1)
  • VersionHistoryItem (26-145)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (3)
packages/lib/package.json (2)

130-139: LGTM! New module exports properly structured.

The new ./permissions and ./monitoring exports are correctly configured and follow the established pattern of other package exports. These align with the PR's goal of exposing centralized rollback permissions and monitoring/activity logging functionality.

Based on learnings, these exports enable consumers to use centralized permission functions from @pagespace/lib/permissions as recommended.


249-254: LGTM! TypeScript mappings correctly added.

The typesVersions entries for permissions and monitoring are properly configured to enable TypeScript type resolution for the new module exports.

apps/web/src/components/version-history/VersionHistoryPanel.tsx (1)

88-95: The history endpoints are already properly authenticated. Both /api/pages/[pageId]/history and /api/drives/[driveId]/history use authenticateRequestWithOptions() to verify requests and include authorization checks (canUserViewPage() and isDriveOwnerOrAdmin()). Browsers automatically send authentication cookies with same-origin fetch requests, so the plain fetch() call is appropriate and secure.

Likely an incorrect or invalid review comment.

Comment thread apps/web/src/components/version-history/VersionHistoryItem.tsx
Comment thread apps/web/src/components/version-history/VersionHistoryPanel.tsx
Add ability to undo AI changes from a specific message point:
- Preview what will be affected before undoing
- Two modes: revert conversation only OR revert with all tool changes
- Activity logging for audit compliance

New files:
- ai-undo-service.ts: preview and execute functions
- UndoAiChangesDialog.tsx: confirmation dialog with mode selection
- /api/ai/chat/messages/[messageId]/undo: GET preview, POST execute

Changes:
- Add conversation_undo operations to activity schema
- Add message rollback handler to rollback-service
- Wire undo button to MessageActionButtons and MessageRenderer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/web/src/services/api/rollback-service.ts (1)

863-866: Operation filter still lacks validation before type assertion.

The operation parameter from user input is directly cast without validation. This was flagged in a previous review and marked as addressed, but the current code still performs an unsafe cast.

🔎 Proposed fix
+    // Valid operation values from the enum
+    const validOperations = ['create', 'update', 'delete', 'restore', 'reorder', 'trash', 'move', 'permission_grant', 'permission_update', 'permission_revoke', 'agent_config_update', 'rollback', 'member_add', 'member_remove', 'member_role_change', 'role_reorder', 'message_update', 'message_delete', 'ownership_transfer', 'conversation_undo'];
+
     if (operation) {
-      // Type assertion needed since operation comes from user input
-      conditions.push(eq(activityLogs.operation, operation as typeof activityLogs.operation.enumValues[number]));
+      if (validOperations.includes(operation)) {
+        conditions.push(eq(activityLogs.operation, operation as typeof activityLogs.operation.enumValues[number]));
+      } else {
+        loggers.api.warn('[RollbackService] Invalid operation filter', { operation });
+      }
     }

Apply the same fix to getDriveVersionHistory at line 940-941.

🧹 Nitpick comments (5)
packages/db/src/schema/monitoring.ts (1)

449-454: Consider adding an index on rollbackFromActivityId.

If queries will filter or join on rollbackFromActivityId (e.g., "find all activities that were rolled back from activity X"), an index would improve performance.

🔎 Add index for rollback queries

Add to the index configuration at the end of the activityLogs table definition (around line 469):

 }, (table) => ({
   timestampIdx: index('idx_activity_logs_timestamp').on(table.timestamp),
   userTimestampIdx: index('idx_activity_logs_user_timestamp').on(table.userId, table.timestamp),
   driveTimestampIdx: index('idx_activity_logs_drive_timestamp').on(table.driveId, table.timestamp),
   pageTimestampIdx: index('idx_activity_logs_page_timestamp').on(table.pageId, table.timestamp),
   archivedIdx: index('idx_activity_logs_archived').on(table.isArchived),
+  rollbackFromIdx: index('idx_activity_logs_rollback_from').on(table.rollbackFromActivityId),
 }));
apps/web/src/services/api/rollback-service.ts (2)

879-883: Inefficient count query fetches all rows instead of using SQL COUNT.

The count query selects all activity log IDs and counts the array length in JavaScript. For large datasets, this loads unnecessary data into memory.

🔎 Proposed fix using SQL count
+import { sql } from '@pagespace/db';
+
     const [activities, countResult] = await Promise.all([
       db
         .select()
         .from(activityLogs)
         .where(and(...conditions))
         .orderBy(desc(activityLogs.timestamp))
         .limit(limit)
         .offset(offset),
       db
-        .select({ count: activityLogs.id })
+        .select({ count: sql<number>`count(*)` })
         .from(activityLogs)
         .where(and(...conditions)),
     ]);

     return {
       activities: activities.map((a) => ({
         // ... mapping
       })),
-      total: countResult.length,
+      total: countResult[0]?.count ?? 0,
     };

Apply the same fix to getDriveVersionHistory at lines 952-956 and 980.

Also applies to: 907-907


844-848: Unused userId parameter in version history functions.

The userId parameter is declared but never used in getPageVersionHistory and getDriveVersionHistory. If it's intended for future authorization checks, consider adding them or removing the parameter.

Also applies to: 921-926

apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx (1)

22-22: Consider simplifying redundant ternary expression.

buttonSize is always 'sm' regardless of the compact value.

🔎 Proposed fix
-  const buttonSize = compact ? 'sm' : 'sm';
+  const buttonSize = 'sm';
apps/web/src/services/api/ai-undo-service.ts (1)

221-227: Consider using if-else-if for clarity.

The context determination uses sequential if statements, which works but is less clear than an if-else-if chain. Each condition should be mutually exclusive.

🔎 Suggested refactor
           // Determine context based on resource type
-          let context: RollbackContext = 'ai_tool';
-          if (activity.resourceType === 'drive') {
+          let context: RollbackContext;
+          if (activity.resourceType === 'drive') {
             context = 'drive';
           } else if (activity.resourceType === 'page') {
             context = 'page';
+          } else {
+            context = 'ai_tool';
           }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 114772b and fb376e5.

📒 Files selected for processing (14)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
  • apps/web/src/components/ai/shared/chat/index.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/index.ts
  • apps/web/src/services/api/rollback-service.ts
  • packages/db/drizzle/0026_nervous_carnage.sql
  • packages/db/drizzle/meta/0026_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema/monitoring.ts
  • packages/lib/src/monitoring/activity-logger.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/src/services/api/index.ts
  • packages/db/drizzle/meta/_journal.json
  • packages/lib/src/monitoring/activity-logger.ts
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{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

Files:

  • apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx
  • apps/web/src/components/ai/shared/chat/index.ts
  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
  • apps/web/src/services/api/rollback-service.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx
  • apps/web/src/components/ai/shared/chat/index.ts
  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
  • apps/web/src/services/api/rollback-service.ts
apps/web/src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.tsx: For document editing, register editing state using useEditingStore.getState().startEditing() and endEditing() to prevent unwanted UI refreshes
For AI streaming operations, register streaming state using useEditingStore.getState().startStreaming() and endStreaming() to prevent unwanted UI refreshes
When using SWR, check useEditingStore state with isAnyActive() and set isPaused to prevent data refreshes during editing or streaming

Files:

  • apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx
  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
**/*.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/ai/shared/chat/MessageActionButtons.tsx
  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx
  • apps/web/src/components/ai/shared/chat/index.ts
  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
  • apps/web/src/services/api/rollback-service.ts
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx
  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
**/*.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/components/ai/shared/chat/index.ts
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/rollback-service.ts
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for database queries with PostgreSQL

Files:

  • packages/db/src/schema/monitoring.ts
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
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);

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
**/*ai*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vercel AI SDK for AI integrations

Files:

  • apps/web/src/services/api/ai-undo-service.ts
🧠 Learnings (10)
📚 Learning: 2025-12-16T19:03:59.870Z
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 91
File: apps/web/src/components/ai/shared/chat/tool-calls/CompactToolCallRenderer.tsx:253-277
Timestamp: 2025-12-16T19:03:59.870Z
Learning: In apps/web/src/components/ai/shared/chat/tool-calls/CompactToolCallRenderer.tsx (TypeScript/React), use the `getLanguageFromPath` utility from `formatters.ts` to infer syntax highlighting language from file paths instead of hardcoding language values in DocumentRenderer calls.

Applied to files:

  • apps/web/src/components/ai/shared/chat/index.ts
  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
📚 Learning: 2025-12-14T14:54:15.319Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.319Z
Learning: Applies to apps/**/*.{ts,tsx} : Always use message parts structure with `parts` array containing objects with `type` and `text` fields when constructing messages for AI

Applied to files:

  • apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx
  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
📚 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/db/src/schema.ts : Maintain the Drizzle ORM database schema in `packages/db/src/schema.ts` as the single entry point for schema definitions

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-14T14:54:15.319Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.319Z
Learning: Applies to apps/web/src/**/*.tsx : For document editing, register editing state using `useEditingStore.getState().startEditing()` and `endEditing()` to prevent unwanted UI refreshes

Applied to files:

  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Message content should always use the message parts structure with `{ parts: [{ type: 'text', text: '...' }] }`

Applied to files:

  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
📚 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 **/*.{ts,tsx} : Always use the message parts structure with `{ parts: [{ type: 'text', text: '...' }] }` format for message content

Applied to files:

  • apps/web/src/components/ai/shared/chat/MessageRenderer.tsx
📚 Learning: 2025-12-16T19:06:20.385Z
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 91
File: apps/web/src/components/ai/ui/Image.tsx:2-2
Timestamp: 2025-12-16T19:06:20.385Z
Learning: In apps/web/src/components/ai/ui/Image.tsx (TypeScript/React), the intentional use of `Experimental_GeneratedImage` from the Vercel AI SDK is accepted. This type is the correct and intended way to handle AI-generated images with base64/mediaType properties, and will be updated when the AI SDK stabilizes this API.

Applied to files:

  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*ai*.{ts,tsx} : Use Vercel AI SDK for AI integrations

Applied to files:

  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/services/api/rollback-service.ts
🧬 Code graph analysis (5)
apps/web/src/components/ai/shared/chat/MessageRenderer.tsx (1)
apps/web/src/components/ai/shared/chat/message-types.ts (1)
  • isProcessedToolPart (87-89)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (3)
apps/web/src/lib/repositories/chat-message-repository.ts (1)
  • chatMessageRepository (47-112)
apps/web/src/services/api/ai-undo-service.ts (3)
  • previewAiUndo (82-173)
  • UndoMode (46-46)
  • executeAiUndo (178-297)
apps/web/src/services/api/index.ts (3)
  • previewAiUndo (60-60)
  • UndoMode (65-65)
  • executeAiUndo (61-61)
apps/web/src/services/api/ai-undo-service.ts (3)
packages/lib/src/permissions/rollback-permissions.ts (1)
  • RollbackContext (15-19)
apps/web/src/services/api/rollback-service.ts (2)
  • previewRollback (134-254)
  • executeRollback (259-359)
packages/lib/src/monitoring/activity-logger.ts (1)
  • logConversationUndo (686-725)
apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx (5)
apps/web/src/components/ai/shared/chat/index.ts (1)
  • UndoAiChangesDialog (21-21)
apps/web/src/services/api/ai-undo-service.ts (2)
  • AiUndoPreview (25-41)
  • UndoMode (46-46)
apps/web/src/services/api/index.ts (2)
  • AiUndoPreview (64-64)
  • UndoMode (65-65)
apps/web/src/lib/auth/auth-fetch.ts (1)
  • fetchWithAuth (704-705)
apps/web/src/components/ui/badge.tsx (1)
  • Badge (46-46)
apps/web/src/services/api/rollback-service.ts (4)
packages/lib/src/monitoring/activity-logger.ts (3)
  • ActivityResourceType (91-105)
  • getActorInfo (24-44)
  • logRollbackActivity (632-679)
packages/db/src/schema/monitoring.ts (1)
  • activityLogs (420-470)
packages/lib/src/permissions/rollback-permissions.ts (2)
  • RollbackContext (15-19)
  • isRollbackableOperation (162-182)
packages/db/src/schema/core.ts (3)
  • pages (24-66)
  • drives (7-22)
  • chatMessages (68-89)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (24)
packages/db/drizzle/0026_nervous_carnage.sql (1)

1-3: LGTM! Migration matches schema definitions.

The enum additions are correctly structured and align with the TypeScript schema definitions in monitoring.ts.

packages/db/src/schema/monitoring.ts (3)

390-395: LGTM! Enum additions are well-structured.

The new operation types support the version history and AI undo features, and the trailing comma on ownership_transfer improves consistency.


411-413: LGTM! Resource enum additions are correct.

The new conversation resource type enables AI conversation tracking and rollback functionality.


490-500: No action required. The retentionPolicies table is properly exported from the monitoring schema module and correctly integrated into the main schema entry point at packages/db/src/schema.ts.

apps/web/src/services/api/rollback-service.ts (3)

396-406: Spreading user-controlled data directly into database update.

The updateData object is built from previousValues which originates from stored activity logs. While the source is semi-trusted, spreading it directly into the .set() call could allow unexpected fields to be written if the activity log was tampered with or contains fields not in the pages schema.

Consider whitelisting allowed fields for page updates similar to how rollbackAgentConfigChange handles it:

+  const allowedPageFields = ['title', 'content', 'parentId', 'position', 'isTrashed'];
+  const safeUpdateData: Record<string, unknown> = {};
+  for (const [key, value] of Object.entries(updateData)) {
+    if (allowedPageFields.includes(key)) {
+      safeUpdateData[key] = value;
+    }
+  }
+
   await db
     .update(pages)
     .set({
-      ...updateData,
+      ...safeUpdateData,
       updatedAt: new Date(),
     })
     .where(eq(pages.id, activity.pageId));

86-129: Well-structured activity fetching with proper error handling.

The getActivityById function correctly maps database results to the ActivityLogForRollback interface, handles missing records gracefully, and logs errors appropriately.


452-547: Permission rollback handlers are now fully implemented.

The rollbackPermissionChange function properly handles all three permission operations (grant, revoke, update) with appropriate database operations and logging. This addresses the concern from the previous review.

apps/web/src/components/ai/shared/chat/index.ts (1)

21-21: LGTM!

The new export follows the established pattern and correctly exposes UndoAiChangesDialog from the shared chat components.

apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx (1)

39-50: LGTM!

The undo button implementation follows the established pattern used by other action buttons, with proper conditional rendering, consistent styling, and accessible title attribute.

apps/web/src/components/ai/shared/chat/MessageRenderer.tsx (2)

244-246: LGTM!

The hasToolCalls computation correctly identifies assistant messages containing tool-call parts, ensuring the undo action is only available when relevant.


354-354: LGTM!

The conditional logic hasToolCalls && onUndoFromHere properly gates the undo callback, and the inline arrow function correctly passes the message ID to the parent handler.

apps/web/src/components/ai/shared/chat/ChatMessagesArea.tsx (2)

66-80: LGTM!

The undo dialog state management is well-implemented with proper useCallback memoization for handler stability and clean separation of concerns between opening, closing, and success callbacks.


163-168: LGTM!

The UndoAiChangesDialog is correctly placed outside the ScrollArea and properly receives the controlled open state, message ID, and success callback.

apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx (2)

93-237: Well-structured dialog with comprehensive UX.

The dialog provides excellent user experience with:

  • Clear loading and error states
  • Summary statistics for messages and changes
  • Mode selection with appropriate disabling when no changes exist
  • Warning display with truncation for long lists
  • Activity badges showing what will be undone
  • Proper button states during execution

18-18: Fix import statement: use post instead of postWithAuth.

The function postWithAuth does not exist in @/lib/auth/auth-fetch. The exported function is named post. Update the import at line 18 to use post.

Note: CSRF token protection is properly implemented in the underlying fetch() method. All POST requests automatically include the X-CSRF-Token header when required (lines 90-99 of auth-fetch.ts).

Likely an incorrect or invalid review comment.

apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (4)

9-10: CSRF protection is properly configured.

The AUTH_OPTIONS_WRITE correctly includes requireCSRF: true, addressing the concern from PR comments about missing CSRF tokens on rollback POST requests. The client-side postWithAuth combined with this server-side check ensures proper CSRF protection.


16-19: Properly handles Next.js 15 async params.

Both route handlers correctly await context.params before destructuring, following the Next.js 15 coding guidelines where params are Promise objects in dynamic routes.

Also applies to: 82-85


96-102: Good input validation for mode parameter.

The mode validation correctly rejects invalid values with a 400 status and clear error message before proceeding with the operation.


138-148: Appropriate use of HTTP 207 Multi-Status for partial success.

Using 207 when some operations succeed but others fail is semantically correct and allows clients to handle partial success scenarios appropriately.

apps/web/src/services/api/ai-undo-service.ts (5)

1-20: LGTM! Clean imports and documentation.

The file header clearly documents the two undo modes, and imports follow the project's ESM conventions using centralized packages.


25-56: LGTM! Well-defined type interfaces.

The type definitions are clear and comprehensive, with no any types.


61-77: LGTM! Clean helper functions.

Both helper functions use Drizzle correctly and have focused responsibilities.


128-135: Fix context determination logic.

The context assignment has a critical flaw:

  1. If resourceType is 'drive', line 131 sets context = 'drive'
  2. But then line 133's else if (activity.isAiGenerated) will always be true (since line 118 filters for isAiGenerated: true), overriding the context to 'ai_tool'
  3. This means drive activities will incorrectly use 'ai_tool' context instead of 'drive' context

Additionally, the condition at line 133 is redundant since all activities in this loop are already AI-generated.

🔎 Proposed fix
       // Determine context based on resource type
-      let context: RollbackContext = 'page';
+      let context: RollbackContext = 'ai_tool';
       if (activity.resourceType === 'drive') {
         context = 'drive';
-      } else if (activity.isAiGenerated) {
-        context = 'ai_tool';
+      } else if (activity.resourceType === 'page') {
+        context = 'page';
       }

Likely an incorrect or invalid review comment.


178-297: Permission checks are already properly implemented in the API route handler at apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts. Both GET and POST handlers validate canUserEditPage(userId, message.pageId) before proceeding, using the centralized permission function from @pagespace/lib/server as required by coding guidelines. No additional permission checks needed in executeAiUndo.

Likely an incorrect or invalid review comment.

Comment thread apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
Comment thread apps/web/src/services/api/ai-undo-service.ts Outdated
Comment thread apps/web/src/services/api/rollback-service.ts
2witstudios and others added 4 commits December 22, 2025 16:16
Required for UndoAiChangesDialog mode selection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Required for radio-group UI component used in UndoAiChangesDialog.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The previous lockfile had a malformed git SSH URL that broke CI:
- Before: git+https://git@github.com:electron/node-gyp.git (broken)
- After: https://codeload.github.com/electron/node-gyp/tar.gz/... (works)

Restored lockfile and re-ran pnpm install to properly resolve dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CRITICAL rules for package manager (always pnpm, never npm)
- Add CRITICAL rules for database migrations (never manually edit)
- Fix retention days: founder=90, business=unlimited (was swapped)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (3)
apps/web/src/services/api/rollback-service.ts (3)

879-883: Optimize count query to use SQL COUNT() instead of fetching all records.

The count query selects all activity log IDs and then counts the array length (lines 880-882). This is inefficient for large datasets. Use Drizzle's SQL count() aggregate instead.

🔎 Proposed fix
+    import { sql } from '@pagespace/db';
+    
     const [activities, countResult] = await Promise.all([
       db
         .select()
         .from(activityLogs)
         .where(and(...conditions))
         .orderBy(desc(activityLogs.timestamp))
         .limit(limit)
         .offset(offset),
       db
-        .select({ count: activityLogs.id })
+        .select({ count: sql<number>`count(*)` })
         .from(activityLogs)
         .where(and(...conditions)),
     ]);

     return {
       activities: activities.map((a) => ({
         // ... mapping
       })),
-      total: countResult.length,
+      total: countResult[0]?.count ?? 0,
     };

Apply the same fix to lines 953-955 in getDriveVersionHistory.


141-151: Unsafe type assertion for null activity.

Casting null to ActivityLogForRollback (line 143) violates type safety and could cause runtime issues if consumers access properties on the returned activity field. Update the RollbackPreview interface to allow activity: ActivityLogForRollback | null and return activity: null directly without the unsafe cast.


863-866: Validate user input before type assertion to prevent query failures.

The operation parameter from user input is directly cast to an enum type without validation (line 865). Add validation before the type assertion to prevent runtime errors and unexpected query behavior.

🔎 Proposed fix
+    // Valid operation values from the enum
+    const validOperations = ['create', 'update', 'delete', 'restore', 'reorder', 'trash', 'move', 'permission_grant', 'permission_update', 'permission_revoke', 'agent_config_update', 'rollback', 'member_add', 'member_remove', 'member_role_change', 'role_reorder', 'message_update', 'message_delete', 'signup', 'login', 'logout', 'ownership_transfer'];
+    
     if (operation) {
+      if (!validOperations.includes(operation)) {
+        loggers.api.warn('[RollbackService] Invalid operation filter', { operation });
+        // Skip invalid operation filter
+      } else {
         conditions.push(eq(activityLogs.operation, operation as typeof activityLogs.operation.enumValues[number]));
+      }
     }

Apply the same fix to line 941 in getDriveVersionHistory.

🧹 Nitpick comments (1)
CLAUDE.md (1)

158-166: Use proper markdown headings instead of bold emphasis.

The new critical rule sections use bold text instead of proper markdown headings. For better document structure and to satisfy markdown linting rules, convert these to level 4 headings.

🔎 Proposed fix
-**CRITICAL: Package Manager**
+#### CRITICAL: Package Manager
 - **ALWAYS use `pnpm`** - This is a pnpm workspace project
 - **NEVER use `npm`** for install, run, or any other commands
 - All scripts in package.json are designed for pnpm

-**CRITICAL: Database Migrations**
+#### CRITICAL: Database Migrations
 - **NEVER manually create or edit SQL migration files** in `packages/db/drizzle/`
 - **ALWAYS use Drizzle generate commands**: `pnpm db:generate`
 - Migration files are auto-generated from schema changes in `packages/db/src/schema/`
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3269154 and 4c68575.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • CLAUDE.md
  • apps/web/src/services/api/rollback-service.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{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

Files:

  • apps/web/src/services/api/rollback-service.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/services/api/rollback-service.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/services/api/rollback-service.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/rollback-service.ts
🧠 Learnings (8)
📚 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/db/drizzle/*.ts : Database migrations should be generated into `packages/db/drizzle/` directory using `pnpm db:generate` command

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to packages/db/{src/schema.ts,drizzle/**/*.ts} : Database schema must be defined in `packages/db/src/schema.ts` and migrations must be emitted to `packages/db/drizzle/`

Applied to files:

  • CLAUDE.md
📚 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 **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • CLAUDE.md
📚 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: Before opening a pull request, run `pnpm build`, `pnpm typecheck`, `pnpm lint`, and relevant database migration tasks

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-12-14T14:54:15.319Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.319Z
Learning: Applies to apps/**/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` for database access instead of direct database connections

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Run `pnpm build`, `pnpm typecheck`, and relevant `db:*` tasks before opening a pull request

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/services/api/rollback-service.ts
🧬 Code graph analysis (1)
apps/web/src/services/api/rollback-service.ts (4)
packages/lib/src/monitoring/activity-logger.ts (2)
  • ActivityResourceType (91-105)
  • logRollbackActivity (632-679)
packages/db/src/schema/monitoring.ts (1)
  • activityLogs (420-470)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/lib/src/permissions/rollback-permissions.ts (3)
  • RollbackContext (15-19)
  • isRollbackableOperation (162-182)
  • canUserRollback (52-141)
🪛 markdownlint-cli2 (0.18.1)
CLAUDE.md

158-158: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


163-163: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests

Comment thread apps/web/src/services/api/rollback-service.ts
2witstudios and others added 6 commits December 22, 2025 16:55
- Add DEFAULT now() for retention_policies.updatedAt column
- Add index on activity_logs.rollbackFromActivityId for queries
- Create migration 0027_fix_retention_updated_at.sql

Addresses PR #118 review issues #10, #20
- Wrap switch case declarations in blocks to prevent variable leakage
- Add isActivityEligibleForRollback() helper for DRY rollback checks
- Export helper for use in history API routes

Addresses PR #118 review issues #5, #13

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add contentFormat field for content type tracking
- Add rollbackFromActivityId for rollback chain references
- Pass fields as top-level properties to logActivity

Addresses PR #118 review issue #3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add permission check before returning activity details
- Implement resourceType query parameter filtering in history routes
- Use shared isActivityEligibleForRollback helper
- Add case-insensitive resource type matching

Addresses PR #118 review issues #6, #7, #13

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use Drizzle count() instead of .length for efficient queries
- Add operation validation against known valid operations
- Fix unsafe null type assertion in RollbackPreview
- Re-export RollbackContext type for API route usage
- Wrap AI undo mutations in database transaction
- Use switch statement for context determination

Addresses PR #118 review issues #2, #16, #18, #19, #23

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use post() with CSRF token for rollback requests
- Add error toast for failed preview fetch
- Handle non-JSON error responses gracefully
- Fix non-existent postWithAuth import to use post
- Remove re-throw after toast notification
- Fix redundant ternary for buttonSize

Addresses PR #118 review issues #1, #8, #11, #17, #22, NEW

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
apps/web/src/app/api/pages/[pageId]/history/route.ts (1)

67-79: Consider clarifying the retention logic comment.

The retention enforcement logic is correct and properly handles unlimited retention (business tier with -1), but the comment could be more explicit about the behavior.

💡 Optional: Make the comment more explicit
-  // Apply retention limit to startDate if not unlimited (-1)
+  // Apply retention limit to startDate (skip if retentionDays <= 0, e.g., -1 for unlimited business tier)
   let effectiveStartDate = params.startDate;
   if (retentionDays > 0) {
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (1)

93-102: Consider using Zod for request body validation.

The manual validation works, but for consistency with the rest of the codebase and better type safety, consider using Zod:

🔎 Suggested Zod validation
+const bodySchema = z.object({
+  mode: z.enum(['messages_only', 'messages_and_changes']),
+});
+
 export async function POST(
   request: Request,
   context: { params: Promise<{ messageId: string }> }
 ) {
   try {
     // Authenticate with CSRF
     const auth = await authenticateRequestWithOptions(request, AUTH_OPTIONS_WRITE);
     if (isAuthError(auth)) return auth.error;
     const userId = auth.userId;

     const { messageId } = await context.params;
-    const body = await request.json();
-    const mode: UndoMode = body.mode;
-
-    // Validate mode
-    if (!mode || !['messages_only', 'messages_and_changes'].includes(mode)) {
-      return NextResponse.json(
-        { error: 'Invalid mode. Must be "messages_only" or "messages_and_changes"' },
-        { status: 400 }
-      );
-    }
+    const body = await request.json();
+    const parseResult = bodySchema.safeParse(body);
+    if (!parseResult.success) {
+      return NextResponse.json(
+        { error: parseResult.error.issues.map(i => i.message).join('. ') },
+        { status: 400 }
+      );
+    }
+    const { mode } = parseResult.data;
apps/web/src/services/api/ai-undo-service.ts (1)

212-259: Partial atomicity is acceptable for partial-success semantics, but document the trade-off.

The rollback loop (lines 213-245) operates outside the transaction, while only message soft-deletion is wrapped. This design supports partial success (some rollbacks fail, messages still get deleted), but means:

  1. If rollbacks succeed but message deletion fails, rollbacks won't be reverted
  2. Rollback failures don't prevent message deletion

This is a reasonable trade-off for UX (users get partial undo rather than all-or-nothing failure), but consider adding a code comment to document this intentional design choice for future maintainers.

🔎 Suggested documentation
     const rolledBackActivityIds: string[] = [];

-    // If mode includes changes, rollback activities in reverse chronological order
+    // If mode includes changes, rollback activities in reverse chronological order.
+    // Note: Rollbacks are performed outside the transaction intentionally to support
+    // partial success - if some rollbacks fail, we still proceed with message deletion.
+    // This means rollback failures don't prevent the undo, and successful rollbacks
+    // won't be reverted if message deletion subsequently fails.
     if (mode === 'messages_and_changes') {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c68575 and 26e5dc3.

📒 Files selected for processing (14)
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/rollback-service.ts
  • packages/db/drizzle/0027_fix_retention_updated_at.sql
  • packages/db/src/schema/monitoring.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • packages/lib/src/permissions/rollback-permissions.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/web/src/components/version-history/VersionHistoryItem.tsx
  • apps/web/src/components/version-history/VersionHistoryPanel.tsx
  • apps/web/src/app/api/drives/[driveId]/history/route.ts
  • apps/web/src/components/ai/shared/chat/UndoAiChangesDialog.tsx
  • apps/web/src/components/ai/shared/chat/MessageActionButtons.tsx
  • packages/lib/src/permissions/rollback-permissions.ts
  • apps/web/src/services/api/rollback-service.ts
🧰 Additional context used
📓 Path-based instructions (8)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{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

Files:

  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
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);

Files:

  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/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/pages/[pageId]/history/route.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/pages/[pageId]/history/route.ts
  • packages/lib/src/monitoring/activity-logger.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/app/api/activities/[activityId]/route.ts
  • packages/db/src/schema/monitoring.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
**/*ai*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vercel AI SDK for AI integrations

Files:

  • apps/web/src/services/api/ai-undo-service.ts
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for database queries with PostgreSQL

Files:

  • packages/db/src/schema/monitoring.ts
🧠 Learnings (2)
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
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/pages/[pageId]/history/route.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
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/activities/[activityId]/route.ts
🧬 Code graph analysis (4)
apps/web/src/app/api/pages/[pageId]/history/route.ts (3)
apps/web/src/lib/auth/index.ts (2)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
apps/web/src/services/api/rollback-service.ts (2)
  • getUserRetentionDays (1022-1052)
  • getPageVersionHistory (870-941)
packages/lib/src/permissions/rollback-permissions.ts (1)
  • isActivityEligibleForRollback (194-203)
apps/web/src/services/api/ai-undo-service.ts (2)
apps/web/src/services/api/rollback-service.ts (3)
  • RollbackContext (16-16)
  • previewRollback (160-280)
  • executeRollback (285-385)
packages/lib/src/monitoring/activity-logger.ts (1)
  • logConversationUndo (692-731)
apps/web/src/app/api/activities/[activityId]/route.ts (3)
apps/web/src/lib/auth/index.ts (2)
  • authenticateRequestWithOptions (216-271)
  • isAuthError (204-206)
packages/lib/src/permissions/rollback-permissions.ts (1)
  • RollbackContext (15-19)
apps/web/src/services/api/index.ts (2)
  • getActivityById (45-45)
  • previewRollback (46-46)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (3)
apps/web/src/lib/repositories/chat-message-repository.ts (1)
  • chatMessageRepository (47-112)
apps/web/src/lib/logging/mask.ts (1)
  • maskIdentifier (1-12)
apps/web/src/services/api/index.ts (3)
  • previewAiUndo (60-60)
  • UndoMode (65-65)
  • executeAiUndo (61-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (21)
apps/web/src/app/api/pages/[pageId]/history/route.ts (4)

1-18: LGTM! Well-structured imports and validation schema.

The imports are appropriate, authentication options correctly disable CSRF for the GET endpoint, and the Zod schema provides comprehensive query parameter validation with sensible constraints (limit capped at 100, offset minimum 0, etc.).


25-36: Excellent Next.js 15 compliance and authentication handling.

The handler correctly awaits context.params before destructuring (Next.js 15 requirement), extracts search parameters using new URL(request.url) as per guidelines, and properly handles authentication with centralized helpers.


38-65: Proper authorization and input validation.

Uses the centralized canUserViewPage() permission function as required by coding guidelines, returns appropriate HTTP status codes (403 for unauthorized, 400 for validation errors), and safely parses query parameters using Zod's safeParse().


81-108: Well-implemented version history fetching and response construction.

The code properly:

  • Passes the retention-adjusted parameters to the service layer
  • Enriches each activity with a canRollback flag using the centralized isActivityEligibleForRollback() helper from the permissions module
  • Returns a well-structured JSON response with NextResponse.json() containing versions, pagination metadata, and retention information
  • Correctly calculates the hasMore pagination flag
packages/db/src/schema/monitoring.ts (3)

390-396: LGTM! Operation enum values correctly extended.

The new rollback and conversation undo operations are properly added and align with the version history feature requirements.


411-413: LGTM! Resource enum extended appropriately.

The message and conversation resource types support the AI conversation undo capabilities.


470-470: LGTM! Index properly defined for rollback chain queries.

The index on rollbackFromActivityId will optimize queries tracing rollback history.

packages/db/drizzle/0027_fix_retention_updated_at.sql (1)

1-5: LGTM! Migration correctly fixes updatedAt default and adds rollback index.

Both changes are necessary and properly implemented:

  • The DEFAULT now() ensures updatedAt is populated on INSERT
  • The IF NOT EXISTS guard prevents conflicts with the index already defined in the schema

Note: Ensure the schema file (packages/db/src/schema/monitoring.ts) is updated to include .defaultNow() on the updatedAt field to match this migration (already flagged in previous review).

apps/web/src/app/api/activities/[activityId]/route.ts (4)

1-12: LGTM on imports and configuration.

Proper use of Zod for query validation, centralized permission functions from @pagespace/lib/permissions, and correct import of RollbackContext type. The AUTH_OPTIONS correctly omits CSRF for this read-only GET endpoint.


19-53: Correct Next.js 15 dynamic route handling and validation.

The handler properly:

  • Types params as Promise<{ activityId: string }> and awaits before destructuring (line 28)
  • Extracts searchParams via new URL(request.url) (line 30)
  • Validates query params with Zod and returns 400 on failure

55-78: Authorization checks are properly implemented.

The granular authorization logic correctly:

  • Verifies page access via canUserViewPage() for page-scoped activities
  • Verifies drive membership via isUserDriveMember() for drive-scoped activities
  • Falls back to ownership check for user-level activities

This addresses the previous review concern about missing authorization.


80-89: LGTM on response handling.

The endpoint correctly computes rollback eligibility via previewRollback and returns a well-structured JSON response using NextResponse.json().

apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (3)

1-11: Well-structured imports and authentication configuration.

Good separation of read vs write auth options. The AUTH_OPTIONS_WRITE correctly enables requireCSRF: true for the POST endpoint, addressing the CSRF concern raised in PR comments.


16-76: GET handler correctly implements preview with authorization.

The handler properly:

  • Awaits context.params before destructuring (Next.js 15 compliant)
  • Validates message existence before permission check
  • Uses centralized canUserEditPage() for authorization
  • Logs permission denials with masked identifiers for privacy
  • Returns appropriate status codes (404, 403, 500)

138-156: Good use of HTTP 207 for partial success scenarios.

The response handling appropriately distinguishes between full success, partial success (207), and complete failure (500). The human-readable messages based on mode provide good UX feedback.

apps/web/src/services/api/ai-undo-service.ts (2)

1-56: Well-defined interfaces and type exports.

The AiUndoPreview and AiUndoResult interfaces are clearly structured with appropriate fields. The UndoMode union type correctly constrains the two supported modes.


82-173: Preview logic is comprehensive and well-structured.

The function correctly:

  • Queries affected messages and AI-generated activities from the conversation point forward
  • Iterates activities to check individual rollback eligibility
  • Aggregates warnings for activities that cannot be rolled back
  • Returns a complete preview with all necessary information for the UI

The N+1 pattern on previewRollback calls is acceptable here given the typical small number of activities per undo operation.

packages/lib/src/monitoring/activity-logger.ts (4)

84-105: LGTM on new operation and resource types.

The new ActivityOperation values ('rollback', 'conversation_undo', 'conversation_undo_with_changes') and ActivityResourceType ('conversation') are properly added to support the version history feature.


127-136: Top-level fields correctly added to ActivityLogInput.

The contentFormat and rollbackFromActivityId fields are now properly defined as top-level optional fields in the interface, addressing the previous review concern about storing them only in metadata.


142-173: logActivity correctly threads new fields to database insert.

Lines 161 and 166 properly pass contentFormat and rollbackFromActivityId as top-level values to the database insert, ensuring they're stored in dedicated columns rather than the generic metadata JSONB field.


687-731: LGTM on logConversationUndo wrapper.

This wrapper correctly:

  • Determines operation based on undo mode
  • Uses 'conversation' as resource type with conversationId as the resource ID
  • Stores conversation-undo-specific metadata (messageId, counts, rolledBackActivityIds)

The design appropriately differs from logRollbackActivity since conversation undo logs the undo event itself rather than a specific resource rollback.

Comment thread apps/web/src/services/api/ai-undo-service.ts Outdated
Comment thread packages/db/src/schema/monitoring.ts Outdated
Comment thread packages/lib/src/monitoring/activity-logger.ts
2witstudios and others added 5 commits December 22, 2025 20:13
Critical & Major fixes:
- Add missing .defaultNow() to retention_policies.updatedAt
  (fixes schema/migration mismatch that would cause insert failures)
- Add contentFormatEnum for type-safe content format validation
  (prevents invalid values during rollback parsing)
- Add CHECK constraint: retentionDays >= -1 (where -1 = unlimited)
- Add rollback source snapshot fields for audit trail preservation:
  - rollbackSourceOperation: captures source activity type
  - rollbackSourceTimestamp: captures when source change occurred
  - rollbackSourceTitle: captures resource title at time of change

These denormalized fields survive retention policy deletion, ensuring
complete audit trails even when source activities are purged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes Major issue #2 from PR #118 review:
- Move rollbackFromActivityId from metadata to top-level field
- Move contentFormat from metadata to top-level field
- Add rollback source snapshot fields to ActivityLogInput interface
- Update logRollbackActivity to accept and pass snapshot fields

Fields are now stored in dedicated database columns instead of being
nested in the metadata JSONB, enabling proper indexing and querying.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enables atomic rollback operations (Issue #7 from PR #118 review):
- Add optional transaction parameter to executeRollback()
- Update all internal rollback functions to accept database parameter
- Pass rollback source snapshot fields to logRollbackActivity

When a transaction is provided, all database operations use it instead
of the default db connection, enabling atomic rollback + message
deletion in AI undo operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes Issues #5 and #7 from PR #118 review:

Context determination (#5):
- Align executeAiUndo context logic with previewAiUndo
- Use 'ai_tool' context for pages (all activities here are AI-generated)
- Simplifies from switch statement to direct assignment with drive check

Transaction atomicity (#7):
- Wrap rollbacks AND message deletion in single transaction
- Pass transaction to executeRollback for atomic operations
- If any operation fails, entire undo is rolled back

This ensures users don't end up in inconsistent states where messages
are deleted but only some changes were reverted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes Issue #6 from PR #118 review:
- Replace manual mode validation with Zod schema
- Aligns with codebase patterns (see rollback route, history route)
- Provides type-safe body parsing with proper TypeScript inference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26e5dc3 and 9163821.

📒 Files selected for processing (8)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/rollback-service.ts
  • packages/db/drizzle/0028_version_history_review_fixes.sql
  • packages/db/drizzle/meta/0028_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema/monitoring.ts
  • packages/lib/src/monitoring/activity-logger.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/db/drizzle/meta/_journal.json
  • packages/lib/src/monitoring/activity-logger.ts
🧰 Additional context used
📓 Path-based instructions (8)
apps/web/src/app/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/app/**/*.ts: In Next.js 15 route handlers with dynamic routes, you MUST await context.params before destructuring because params are Promise objects
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON responses using return Response.json(data) or return NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{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

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/rollback-service.ts
  • packages/db/src/schema/monitoring.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/rollback-service.ts
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);

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/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/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/rollback-service.ts
  • packages/db/src/schema/monitoring.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/rollback-service.ts
  • packages/db/src/schema/monitoring.ts
**/*ai*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vercel AI SDK for AI integrations

Files:

  • apps/web/src/services/api/ai-undo-service.ts
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for database queries with PostgreSQL

Files:

  • packages/db/src/schema/monitoring.ts
🧠 Learnings (5)
📚 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/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/services/api/rollback-service.ts
📚 Learning: 2025-12-22T22:46:00.813Z
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 118
File: apps/web/src/services/api/rollback-service.ts:994-1024
Timestamp: 2025-12-22T22:46:00.813Z
Learning: In PageSpace, the subscription tier hierarchy is: free < pro < founder < business, where Business is the highest tier with unlimited retention (-1 days), Founder has 90 days, Pro has 30 days, and Free has 7 days retention.

Applied to files:

  • apps/web/src/services/api/rollback-service.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/db/src/schema.ts : Maintain the Drizzle ORM database schema in `packages/db/src/schema.ts` as the single entry point for schema definitions

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • packages/db/src/schema/monitoring.ts
🧬 Code graph analysis (1)
apps/web/src/services/api/ai-undo-service.ts (5)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/db/src/schema/monitoring.ts (1)
  • activityLogs (422-476)
apps/web/src/services/api/rollback-service.ts (3)
  • RollbackContext (16-16)
  • previewRollback (161-281)
  • executeRollback (287-393)
apps/processor/src/logger.ts (1)
  • error (57-63)
packages/lib/src/monitoring/activity-logger.ts (2)
  • getActorInfo (24-44)
  • logConversationUndo (704-743)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (10)
packages/db/drizzle/0028_version_history_review_fixes.sql (1)

1-25: LGTM! Well-structured migration with proper safety measures.

The migration correctly introduces:

  • Safe enum creation with duplicate_object exception handling
  • Type conversion using USING cast (preserves NULLs, fails on invalid values)
  • Denormalized rollback source fields for audit trail preservation
  • CHECK constraint enforcing valid retention days (>= -1)

All changes align with the schema updates and rollback infrastructure introduced in this PR.

apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (2)

22-82: LGTM! GET handler follows Next.js 15 and coding guidelines correctly.

The handler properly:

  • Awaits context.params before destructuring (Next.js 15 requirement)
  • Uses centralized permission checking via canUserEditPage()
  • Returns responses with NextResponse.json()
  • Includes structured logging with masked identifiers

88-173: LGTM! POST handler correctly implements CSRF protection and follows all guidelines.

The handler properly:

  • Awaits context.params and request.json() (Next.js 15 patterns)
  • Requires CSRF protection via AUTH_OPTIONS_WRITE with requireCSRF: true
  • Validates request body with Zod 4 (correct z.enum() usage)
  • Uses centralized permission function canUserEditPage()
  • Returns appropriate HTTP status codes (207 for partial success, 500 for failure)

Note: The PR objectives mention frontend POST requests missing CSRF tokens. This is a frontend issue (likely in VersionHistoryPanel.tsx), not an issue with this route handler, which correctly enforces CSRF protection.

packages/db/src/schema/monitoring.ts (2)

390-398: LGTM! Enum additions align with migrations and rollback infrastructure.

The new enum values correctly support:

  • Rollback operations ('rollback', 'conversation_undo', 'conversation_undo_with_changes')
  • Content format validation (contentFormatEnum with text/html/json/tiptap)
  • AI conversation resources ('conversation')

All changes match the corresponding SQL migrations (0025, 0026, 0028).


451-476: LGTM! Activity logs schema correctly implements rollback support.

The changes properly address previous review feedback:

  • contentFormat now uses the contentFormatEnum for type safety (previously flagged as needing validation)
  • Rollback tracking fields are appropriately nullable for audit trail preservation
  • Index on rollbackFromActivityId supports efficient rollback queries

All fields match migration 0028 and support the rollback infrastructure introduced in this PR.

apps/web/src/services/api/rollback-service.ts (5)

28-45: LGTM! Input validation prevents SQL injection and query errors.

The VALID_OPERATIONS constant and isValidOperation() function correctly address the previous review concern about validating user input before type assertions. This validation is properly applied in both:

  • getPageVersionHistory() (line 904)
  • getDriveVersionHistory() (line 980)

This prevents invalid operation values from causing runtime errors or potential security issues.


287-393: LGTM! Transaction support correctly implemented for atomic rollbacks.

The executeRollback() function properly:

  • Accepts optional tx parameter for transaction context (line 291)
  • Passes transaction to all resource-specific rollback handlers
  • Allows atomic rollbacks when called within a transaction (as done in ai-undo-service.ts)

This addresses the previous review feedback about wrapping operations in transactions.


488-822: LGTM! Resource-specific rollback handlers are now fully implemented.

The handlers for permissions, members, and roles now correctly perform database operations instead of just logging warnings (as flagged in previous reviews):

  • rollbackPermissionChange() (lines 488-584): Handles grant/revoke/update with actual DB operations
  • rollbackMemberChange() (lines 637-730): Handles add/remove/role changes with DB operations
  • rollbackRoleChange() (lines 735-822): Handles create/delete/update with DB operations

All handlers properly accept the database parameter for transaction support and include structured logging.


911-923: LGTM! Count queries optimized to use SQL COUNT() aggregate.

Both history functions now correctly use Drizzle's count() aggregate function instead of fetching all rows and counting in JavaScript (lines 919-920, 995-996). This addresses the previous performance concern and prevents memory issues with large datasets.

Also applies to: 987-999


1037-1067: LGTM! Retention tier mapping is correct per product specifications.

The retention days mapping correctly implements the tier hierarchy:

  • Free: 7 days
  • Pro: 30 days
  • Founder: 90 days
  • Business: unlimited (-1)

Based on learnings, this ordering is correct with Business as the highest tier.

Comment thread apps/web/src/services/api/ai-undo-service.ts
Comment thread packages/db/src/schema/monitoring.ts Outdated
- Add CHECK constraint to retentionPolicies schema definition
  (aligns Drizzle schema with existing migration constraint)
- Change AI undo to all-or-nothing transaction semantics
  (any rollback failure aborts entire operation)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
packages/db/src/schema/monitoring.ts (1)

457-457: Foreign key constraint for rollback tracking still not added.

As noted in previous reviews, rollbackFromActivityId lacks a foreign key constraint to activityLogs.id. This design choice allows rollback provenance to survive source activity deletion, which may be intentional for audit compliance. If intentional, consider adding a comment explaining this decision.

🧹 Nitpick comments (2)
packages/db/src/schema/monitoring.ts (1)

453-453: Consider storage implications for contentSnapshot.

The contentSnapshot field is unbounded text, which could lead to significant database growth when storing large page content. Consider whether content snapshots need size limits, compression, or external storage for large payloads.

apps/web/src/services/api/ai-undo-service.ts (1)

189-200: Optional: Consider reusing message from preview.

The message is fetched twice: once inside previewAiUndo (line 88) and again at line 200. While this doesn't affect correctness, you could optimize by including the message object in the AiUndoPreview return type and reusing it here to save a database query.

This is a minor optimization and the current approach maintains clean separation of concerns.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9163821 and bc8e023.

📒 Files selected for processing (2)
  • apps/web/src/services/api/ai-undo-service.ts
  • packages/db/src/schema/monitoring.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use any types in TypeScript code - always use proper TypeScript types

**/*.{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

Files:

  • apps/web/src/services/api/ai-undo-service.ts
  • packages/db/src/schema/monitoring.ts
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/**/*.{ts,tsx}: Use centralized permission functions from @pagespace/lib/permissions for access control, such as getUserAccessLevel() and canUserEditPage()
Always use Drizzle client from @pagespace/db for database access instead of direct database connections
Always use message parts structure with parts array containing objects with type and text fields when constructing messages for AI

Files:

  • apps/web/src/services/api/ai-undo-service.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/services/api/ai-undo-service.ts
  • packages/db/src/schema/monitoring.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/ai-undo-service.ts
  • packages/db/src/schema/monitoring.ts
**/*ai*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vercel AI SDK for AI integrations

Files:

  • apps/web/src/services/api/ai-undo-service.ts
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for database queries with PostgreSQL

Files:

  • packages/db/src/schema/monitoring.ts
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`
📚 Learning: 2025-12-14T14:54:15.319Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.319Z
Learning: Applies to apps/**/*.{ts,tsx} : Always use message parts structure with `parts` array containing objects with `type` and `text` fields when constructing messages for AI

Applied to files:

  • apps/web/src/services/api/ai-undo-service.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/db/src/schema.ts : Maintain the Drizzle ORM database schema in `packages/db/src/schema.ts` as the single entry point for schema definitions

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to packages/db/src/schema.ts : Database schema entry point is at `packages/db/src/schema.ts`; migrations emit to `packages/db/drizzle/`

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to packages/db/{src/schema.ts,drizzle/**/*.ts} : Database schema must be defined in `packages/db/src/schema.ts` and migrations must be emitted to `packages/db/drizzle/`

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to packages/db/**/*.{ts,tsx} : Use Drizzle ORM for database queries with PostgreSQL

Applied to files:

  • packages/db/src/schema/monitoring.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 **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • packages/db/src/schema/monitoring.ts
📚 Learning: 2025-12-14T14:54:15.319Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-14T14:54:15.319Z
Learning: Applies to apps/**/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` for database access instead of direct database connections

Applied to files:

  • packages/db/src/schema/monitoring.ts
🧬 Code graph analysis (2)
apps/web/src/services/api/ai-undo-service.ts (4)
packages/db/src/schema/core.ts (1)
  • chatMessages (68-89)
packages/db/src/schema/monitoring.ts (1)
  • activityLogs (423-477)
apps/web/src/services/api/rollback-service.ts (3)
  • RollbackContext (16-16)
  • previewRollback (161-281)
  • executeRollback (287-393)
packages/lib/src/monitoring/activity-logger.ts (2)
  • getActorInfo (24-44)
  • logConversationUndo (704-743)
packages/db/src/schema/monitoring.ts (2)
packages/lib/src/sheets/sheet.ts (1)
  • check (1428-1440)
packages/db/src/index.ts (1)
  • sql (8-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (11)
packages/db/src/schema/monitoring.ts (6)

15-18: LGTM! Import additions support new schema features.

The additional imports (pgEnum, check, relations, sql) are appropriately used throughout the file for the new enums, constraints, and relations.


391-396: LGTM! Operation enum extended for version history and AI undo features.

The new operations ('rollback', 'conversation_undo', 'conversation_undo_with_changes') align with the PR objectives for version history and AI conversation undo functionality.


399-399: LGTM! Content format enum provides type safety.

This enum addresses previous review feedback by ensuring only valid content formats ('text', 'html', 'json', 'tiptap') can be stored, providing database-level validation for the contentFormat field.


414-416: LGTM! Resource enum extended for messages and conversations.

The additions ('message', 'conversation') support the new AI conversation undo functionality and align with the operation enum extensions.


476-476: LGTM! Index improves rollback chain query performance.

The index on rollbackFromActivityId will efficiently support queries that traverse or filter rollback chains.


508-508: LGTM! CHECK constraint addresses previous feedback.

The valid_retention_days constraint ensures retentionDays >= -1, which correctly enforces the unlimited (-1) and positive day values as specified in the requirements.

apps/web/src/services/api/ai-undo-service.ts (5)

10-20: LGTM! Clean imports and proper package usage.

The imports correctly use Drizzle from @pagespace/db, monitoring utilities from @pagespace/lib/monitoring, and the rollback service for integration. All ESM imports follow the codebase conventions.


22-56: LGTM! Well-defined TypeScript types.

The type definitions are clear and comprehensive, with proper JSDoc documentation. All types follow TypeScript best practices with no any types.


58-77: LGTM! Clean helper functions.

Both helper functions use Drizzle's query builder correctly and handle missing data gracefully by returning null values.


79-173: LGTM! Comprehensive preview implementation.

The preview function correctly:

  • Queries all affected messages and AI-generated activities
  • Evaluates rollback eligibility for each activity with appropriate context
  • Collects warnings for non-rollbackable items
  • Handles errors gracefully with logging

The context determination logic properly handles drive, page, and AI-generated activities.


175-299: Excellent implementation! All past issues resolved.

The execute function now implements proper all-or-nothing transaction semantics:

  • All operations (rollbacks + message deletions) wrapped in a single transaction
  • Any failure throws an error to abort the entire transaction
  • Context determination is consistent with preview logic

The transaction flow correctly handles all scenarios:

  • Non-rollbackable activities abort the transaction
  • Failed rollbacks abort the transaction
  • Failed message deletions abort the transaction
  • Success only when all operations complete

Comment thread packages/db/src/schema/monitoring.ts Outdated
- Create subscription_tier pgEnum with 'free', 'pro', 'business', 'founder'
- Convert retentionPolicies.subscriptionTier from text to enum
- Adds DB-level validation to prevent invalid tier values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2witstudios and others added 4 commits December 23, 2025 09:23
- Add route tests for /api/activities/[activityId]/rollback (12 tests)
- Add route tests for /api/pages/[pageId]/history (21 tests)
- Add service tests for ai-undo-service (16 tests) with @scaffold label
- Add service tests for rollback-service (28 tests) with @scaffold label
- Add permission tests for rollback-permissions (69 tests)
- Fix undo route test: remove duplicate test expecting wrong status
- Add fake timers to history route tests for deterministic dates

Per rubric v2: service tests use @scaffold labels for ORM chain mocks
pending repository seam refactoring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add "Restore this version" action to activity items in:
- ActivityDashboard (middle panel for /dashboard/activity and drive activity)
- SidebarActivityTab (right sidebar context-aware activity feed)

Changes:
- ActivityItem: Add hover dropdown menu with rollback action and confirmation dialog
- ActivityTimeline: Pass context and onRollback handler to items
- ActivityDashboard: Handle rollback with proper context mapping (user→user_dashboard, drive→drive)
- SidebarActivityTab: Add rollback UI with context-aware scoping (page/drive/user_dashboard)

Rollback is properly scoped by context to prevent unintended changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add compliance tests for:
- logRollbackActivity: validates rollback operation logging with source activity reference
- logConversationUndo: validates conversation undo logging for both modes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 'create' as rollbackable operation (trash resource to undo creation)
- Support both global messages and page chat messages in rollback
- Fix AI undo timing to include tool calls from preceding message
- Handle message create rollback by deactivating the message

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts (1)

342-357: Add clarifying comment for partial failure mock.

This test mocks success: false with messagesDeleted: 3, which may be inconsistent with the service's transactional behavior (where failed transactions would roll back all changes). While valid as a unit test of route handler defensiveness, a brief comment would help future maintainers understand the intent.

🧹 Nitpick comments (7)
apps/web/src/app/api/pages/[pageId]/history/__tests__/route.test.ts (1)

139-149: Consider adding assertion that service is not called on authorization failure.

Similar to the authentication test, it would be valuable to verify getPageVersionHistory is not called when the user lacks view permission.

🔎 Suggested enhancement
     it('returns 403 when user cannot view page', async () => {
       (canUserViewPage as Mock).mockResolvedValue(false);

       const response = await GET(createRequest(), { params: mockParams });
       const body = await response.json();

       expect(response.status).toBe(403);
       expect(body.error).toContain('do not have access');
+      expect(getPageVersionHistory).not.toHaveBeenCalled();
     });
apps/web/src/services/api/__tests__/ai-undo-service.test.ts (2)

93-114: Consider adding return type annotations to factory functions.

The factory functions work correctly but lack explicit return types. Adding type annotations would improve type safety and documentation.

🔎 Suggested improvement
-const createMockMessage = (overrides = {}) => ({
+const createMockMessage = (overrides: Partial<ReturnType<typeof createMockMessage>> = {}): {
+  id: string;
+  conversationId: string;
+  pageId: string;
+  createdAt: Date;
+  role: string;
+  content: string;
+  isActive: boolean;
+} => ({
   id: mockMessageId,
   conversationId: mockConversationId,
   pageId: mockPageId,
   createdAt: new Date('2024-01-15T10:00:00Z'),
   role: 'user',
   content: 'Test message',
   isActive: true,
   ...overrides,
 });

Alternatively, if there are shared types in the codebase for these entities, import and use those types directly.


459-463: Redundant try/catch block.

The try/catch that only rethrows the error adds no value. The mock can simply await the callback without the try/catch wrapper.

🔎 Proposed simplification
       (db.transaction as Mock).mockImplementation(async (callback) => {
         const tx = {
           update: vi.fn().mockReturnValue({
             set: vi.fn().mockReturnValue({
               where: vi.fn().mockResolvedValue(undefined),
             }),
           }),
         };
-        try {
-          await callback(tx);
-        } catch (e) {
-          throw e;
-        }
+        await callback(tx);
       });

The same pattern appears at lines 504-508.

packages/lib/src/permissions/__tests__/rollback-permissions.test.ts (1)

114-124: Avoid as any casts per coding guidelines.

Lines 115 and 123 use as any for type casting. Consider using a more specific type assertion or adjusting the test approach to satisfy TypeScript's type checker without any.

🔎 Suggested fix
-      it.each(rollbackableTypes)('returns true for %s resource type', (type) => {
-        expect(isRollbackableResourceType(type as any)).toBe(true);
+      it.each(rollbackableTypes)('returns true for %s resource type', (type) => {
+        expect(isRollbackableResourceType(type as ActivityResourceType)).toBe(true);
       });
     });

     describe('non-rollbackable resource types', () => {
       const nonRollbackableTypes = ['user', 'file', 'token', 'device', 'conversation'];

       it.each(nonRollbackableTypes)('returns false for %s resource type', (type) => {
-        expect(isRollbackableResourceType(type as any)).toBe(false);
+        expect(isRollbackableResourceType(type as ActivityResourceType)).toBe(false);
       });

You'll need to import ActivityResourceType from the appropriate module.

apps/web/src/components/activity/ActivityItem.tsx (1)

42-67: Consider extracting shared rollback preview/confirm logic.

The handleRestoreClick and handleConfirmRollback pattern here is similar to SidebarActivityTab.tsx. Consider extracting this into a shared hook (e.g., useRollbackPreview) to reduce duplication.

apps/web/src/services/api/__tests__/rollback-service.test.ts (1)

798-801: Fragile Promise.all spy approach.

Spying on Promise.all is brittle because:

  1. It affects all Promise.all calls globally during the test
  2. Implementation changes that don't use Promise.all would break tests

Consider restructuring the mock to return appropriate data from the database mock chain instead, or acknowledge this as a temporary scaffold.

Also applies to: 824-827, 886-889, 911-914

apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts (1)

168-192: Consider expanding response field assertions.

The success test verifies messagesAffected and activitiesAffected, but doesn't verify other returned fields from the mock (messageId, conversationId, pageId, driveId, warnings). Consider adding assertions for additional fields to strengthen the contract test.

🔎 Suggested additional assertions
       expect(response.status).toBe(200);
       expect(body.messagesAffected).toBe(5);
       expect(body.activitiesAffected).toHaveLength(1);
+      expect(body.messageId).toBe(mockMessageId);
+      expect(body.pageId).toBe(mockPageId);
+      expect(body.warnings).toEqual([]);
     });
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ebf55c6 and 6e08706.

📒 Files selected for processing (10)
  • apps/web/src/app/api/activities/[activityId]/rollback/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/pages/[pageId]/history/__tests__/route.test.ts
  • apps/web/src/components/activity/ActivityDashboard.tsx
  • apps/web/src/components/activity/ActivityItem.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/services/api/__tests__/ai-undo-service.test.ts
  • apps/web/src/services/api/__tests__/rollback-service.test.ts
  • packages/lib/src/permissions/__tests__/rollback-permissions.test.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{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
Always use message parts structure for message content: { parts: [{ type: 'text', text: 'content' }] }
Use centralized permissions via import { getUserAccessLevel, canUserEditPage } from '@pagespace/lib/permissions';
Always use Drizzle client from @pagespace/db for database access: import { db, pages } from '@pagespace/db';

Files:

  • apps/web/src/services/api/__tests__/rollback-service.test.ts
  • packages/lib/src/permissions/__tests__/rollback-permissions.test.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/activities/[activityId]/rollback/__tests__/route.test.ts
  • apps/web/src/services/api/__tests__/ai-undo-service.test.ts
  • apps/web/src/components/activity/ActivityItem.tsx
  • apps/web/src/app/api/pages/[pageId]/history/__tests__/route.test.ts
  • apps/web/src/components/activity/ActivityDashboard.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
**/*.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/services/api/__tests__/rollback-service.test.ts
  • packages/lib/src/permissions/__tests__/rollback-permissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/activities/[activityId]/rollback/__tests__/route.test.ts
  • apps/web/src/services/api/__tests__/ai-undo-service.test.ts
  • apps/web/src/app/api/pages/[pageId]/history/__tests__/route.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/__tests__/rollback-service.test.ts
  • packages/lib/src/permissions/__tests__/rollback-permissions.test.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/activities/[activityId]/rollback/__tests__/route.test.ts
  • apps/web/src/services/api/__tests__/ai-undo-service.test.ts
  • apps/web/src/components/activity/ActivityItem.tsx
  • apps/web/src/app/api/pages/[pageId]/history/__tests__/route.test.ts
  • apps/web/src/components/activity/ActivityDashboard.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Register editing state using useEditingStore.getState().startEditing() and .endEditing() to prevent UI refreshes during document editing
Register streaming state using useEditingStore.getState().startStreaming() and .endStreaming() to prevent UI refreshes during AI streaming
Use SWR with isPaused() to prevent refetches during editing, but always allow the initial fetch to complete before pausing

Files:

  • apps/web/src/services/api/__tests__/rollback-service.test.ts
  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/activities/[activityId]/rollback/__tests__/route.test.ts
  • apps/web/src/services/api/__tests__/ai-undo-service.test.ts
  • apps/web/src/components/activity/ActivityItem.tsx
  • apps/web/src/app/api/pages/[pageId]/history/__tests__/route.test.ts
  • apps/web/src/components/activity/ActivityDashboard.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
**/*.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/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/ActivityItem.tsx
  • apps/web/src/components/activity/ActivityDashboard.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
**/*.{tsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
  • apps/web/src/components/activity/ActivityItem.tsx
  • apps/web/src/components/activity/ActivityDashboard.tsx
  • apps/web/src/components/activity/ActivityTimeline.tsx
**/*ai*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vercel AI SDK for AI integrations

Files:

  • apps/web/src/services/api/__tests__/ai-undo-service.test.ts
🧠 Learnings (2)
📚 Learning: 2025-12-18T05:22:42.263Z
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 96
File: apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarChatTab.tsx:641-657
Timestamp: 2025-12-18T05:22:42.263Z
Learning: In apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarChatTab.tsx, the provider/model selector buttons are intentionally non-functional placeholders in the compact sidebar view. The `hideModelSelector={true}` prop is passed to ChatInput to hide the full ProviderModelSelector. Users are expected to use the full GlobalAssistantView for model selection. A settings link may be added in a future iteration.

Applied to files:

  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Use Next.js 15 App Router with TypeScript

Applied to files:

  • apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx
🧬 Code graph analysis (9)
apps/web/src/services/api/__tests__/rollback-service.test.ts (3)
packages/db/src/index.ts (1)
  • db (20-20)
packages/lib/src/permissions/rollback-permissions.ts (1)
  • isRollbackableOperation (167-187)
packages/lib/src/monitoring/activity-logger.ts (1)
  • logRollbackActivity (644-697)
packages/lib/src/permissions/__tests__/rollback-permissions.test.ts (1)
packages/lib/src/permissions/rollback-permissions.ts (5)
  • ActivityForPermissionCheck (24-33)
  • isRollbackableOperation (167-187)
  • isRollbackableResourceType (151-162)
  • isActivityEligibleForRollback (194-203)
  • canUserRollback (52-146)
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (5)
apps/web/src/hooks/useToast.ts (1)
  • useToast (13-43)
apps/web/src/components/activity/ActivityItem.tsx (1)
  • ActivityItem (29-153)
apps/web/src/lib/auth/auth-fetch.ts (2)
  • fetchWithAuth (704-705)
  • fetch (44-211)
apps/web/src/components/ui/dropdown-menu.tsx (4)
  • DropdownMenu (242-242)
  • DropdownMenuTrigger (244-244)
  • DropdownMenuContent (245-245)
  • DropdownMenuItem (248-248)
apps/web/src/components/ui/button.tsx (1)
  • Button (59-59)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts (2)
apps/web/src/services/api/ai-undo-service.ts (2)
  • previewAiUndo (131-227)
  • executeAiUndo (232-345)
apps/web/src/lib/repositories/global-conversation-repository.ts (1)
  • globalConversationRepository (135-330)
apps/web/src/services/api/__tests__/ai-undo-service.test.ts (2)
packages/db/src/index.ts (1)
  • db (20-20)
packages/lib/src/monitoring/activity-logger.ts (1)
  • logConversationUndo (704-743)
apps/web/src/components/activity/ActivityItem.tsx (6)
apps/web/src/components/activity/types.ts (1)
  • ActivityLog (10-31)
apps/web/src/components/activity/index.ts (1)
  • ActivityItem (3-3)
apps/web/src/hooks/useToast.ts (1)
  • useToast (13-43)
apps/web/src/components/activity/constants.ts (2)
  • operationConfig (17-30)
  • defaultOperationConfig (39-43)
apps/web/src/components/ui/dropdown-menu.tsx (4)
  • DropdownMenu (242-242)
  • DropdownMenuTrigger (244-244)
  • DropdownMenuContent (245-245)
  • DropdownMenuItem (248-248)
apps/web/src/components/ui/button.tsx (1)
  • Button (59-59)
apps/web/src/app/api/pages/[pageId]/history/__tests__/route.test.ts (1)
packages/lib/src/permissions/rollback-permissions.ts (1)
  • isActivityEligibleForRollback (194-203)
apps/web/src/components/activity/ActivityDashboard.tsx (1)
apps/web/src/components/activity/ActivityItem.tsx (1)
  • RollbackContext (21-21)
apps/web/src/components/activity/ActivityTimeline.tsx (1)
apps/web/src/components/activity/ActivityItem.tsx (2)
  • RollbackContext (21-21)
  • ActivityItem (29-153)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (31)
apps/web/src/app/api/pages/[pageId]/history/__tests__/route.test.ts (8)

1-42: LGTM! Mock setup is well-organized.

The mock hoisting pattern with vi.mock before imports is correct for Vitest. The separation of @pagespace/lib and @pagespace/lib/permissions mocks handles the different module entry points appropriately.


43-67: LGTM! Test helpers are well-structured.

The mockParams as a Promise aligns with Next.js 15's async params pattern. The helper functions provide clean abstractions for test setup.


69-86: LGTM! Factory function provides flexible test data creation.

The default values correctly represent a rollback-eligible activity (with previousValues set), and the spread pattern allows easy customization per test case.


88-106: LGTM! Test setup with fake timers ensures deterministic retention calculations.

The beforeEach/afterEach pattern properly isolates tests, and the fixed system time (2024-06-15) enables reliable date arithmetic in retention tests.


108-133: LGTM! Authentication tests cover key scenarios.

Good practice verifying that getPageVersionHistory is not called when authentication fails, ensuring proper request isolation.


155-251: LGTM! Query parameter tests cover the main validation contract.

The tests validate bounds (limit ≤ 100, offset ≥ 0) and filter passthrough correctly. Optional enhancement: consider adding tests for malformed inputs (non-numeric strings, invalid date formats) if the route handler is expected to handle them gracefully.


257-358: LGTM! Retention tests comprehensively cover tier-based limits.

The use of fake timers ensures deterministic date calculations. The tolerance range (9-11 days) in line 320-321 appropriately handles potential boundary conditions in date arithmetic.


364-416: LGTM! Response format tests verify the API contract accurately.

The sequential mockReturnValueOnce calls correctly test that canRollback is computed per-activity. Pagination hasMore logic is verified with proper boundary cases.

apps/web/src/services/api/__tests__/ai-undo-service.test.ts (2)

1-18: Good documentation of testing approach.

The scaffold comment clearly documents the trade-off with order-dependent ORM chain mocks and suggests a future improvement path (repository seam). This transparency helps maintainers understand test fragility.


626-691: Good edge case coverage.

The tests for single-message scenarios and pages without driveId (global assistant) cover important boundary conditions that could easily be overlooked.

packages/lib/src/permissions/__tests__/rollback-permissions.test.ts (1)

1-370: Well-structured test suite with comprehensive coverage.

The test file follows good practices:

  • Clear separation between pure function tests and permission check tests
  • Proper mocking at the boundary (permissions module)
  • Good use of beforeEach for mock cleanup
  • Comprehensive coverage of edge cases including unknown contexts
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarActivityTab.tsx (2)

249-277: Rollback execution correctly uses post() helper.

The rollback POST request uses the post() helper which should handle CSRF tokens automatically, unlike the issue reported in VersionHistoryPanel.tsx. This implementation is consistent with the codebase patterns.


411-430: Clean dropdown menu implementation for rollback actions.

The rollback action UI is well-implemented with appropriate hover behavior via the group class and transition effects. Good use of accessibility with sr-only for screen readers.

apps/web/src/components/activity/ActivityTimeline.tsx (2)

5-31: Clean prop threading for rollback functionality.

The changes correctly:

  • Import RollbackContext type from ActivityItem
  • Add optional props to maintain backward compatibility
  • Thread context and onRollback to child components

61-66: Props correctly passed to ActivityItem.

The context and onRollback props are properly propagated to each ActivityItem, enabling context-aware rollback behavior.

apps/web/src/components/activity/ActivityDashboard.tsx (2)

209-230: Rollback handler correctly implemented.

The handleRollback function:

  • Uses post() helper which handles CSRF automatically
  • Maps context correctly (user → user_dashboard)
  • Has proper error handling with user feedback via toast
  • Refreshes the activity list after successful rollback

327-328: Rollback props correctly wired to ActivityTimeline.

The context and onRollback props are properly passed to enable rollback functionality throughout the timeline.

apps/web/src/app/api/activities/[activityId]/rollback/__tests__/route.test.ts (3)

77-92: Good CSRF validation test.

This test verifies that the route handler requires CSRF protection, which aligns with the PR requirement. The test confirms requireCSRF: true is passed to the authentication options.


130-146: Comprehensive context validation testing.

Tests all valid context values (page, drive, ai_tool, user_dashboard) ensuring the route accepts each. Good coverage of the validation boundary.


1-271: Well-structured contract test suite.

The test file follows good practices:

  • Clear documentation of test contract
  • Proper mocking at service boundaries
  • Comprehensive coverage of auth, validation, dry-run, and execution paths
  • Helper functions for request/auth creation improve readability
apps/web/src/components/activity/ActivityItem.tsx (2)

21-21: Good type export for reuse.

Exporting RollbackContext as a type allows other components (like ActivityTimeline and ActivityDashboard) to import and use it, promoting type consistency across the rollback feature.


119-150: Well-implemented rollback UI with confirmation dialog.

Good implementation:

  • Conditional rendering based on canRestore
  • Proper accessibility with sr-only text
  • Clean integration with RollbackConfirmDialog
  • Fragment wrapper handles multiple root elements correctly
apps/web/src/services/api/__tests__/rollback-service.test.ts (2)

1-15: Good scaffold documentation.

The comment clearly explains the mocking strategy and references the rubric for future repository abstraction. This helps future maintainers understand why the complex mocking approach was chosen.


402-458: Comprehensive rollback execution test with audit logging verification.

Good test that verifies:

  • Database update is performed
  • Audit log (logRollbackActivity) is called with correct parameters
  • Success response includes restored values
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts (7)

1-55: Well-structured mock setup.

The mock configuration properly isolates the route handler from its dependencies, covering the service boundary, authentication, repository, permissions, and logging. This follows good testing patterns.


57-88: LGTM!

Test helpers are well-typed and follow good patterns. The mockParams as a Promise correctly matches Next.js 13+ dynamic route params API.


90-162: Good coverage of authentication and authorization paths.

Tests properly verify:

  • 401 when unauthenticated (and service not invoked)
  • 404 when preview returns null
  • 403 for both page edit permission and global conversation ownership

The authorization tests cover both page_chat and global_chat sources, which is thorough.


222-229: Good CSRF requirement verification.

This test confirms the endpoint requires CSRF protection by verifying requireCSRF: true is passed to authenticateRequestWithOptions. This aligns with the PR comment noting that frontend POST requests need to include CSRF tokens.


236-279: Comprehensive input validation coverage.

The validation tests appropriately cover:

  • Invalid mode rejection (400)
  • Missing mode rejection (400)
  • Both valid modes accepted (messages_only, messages_and_changes)

Including "accepts valid input" tests in the validation block is a good practice for completeness.


301-335: LGTM!

Success tests properly verify both modes return 200 with appropriate response bodies, including the human-readable message format.


359-372: LGTM!

The complete failure test correctly verifies that when no operations succeed (messagesDeleted: 0, activitiesRolledBack: 0), the endpoint returns 500 with success: false.

Comment thread apps/web/src/components/activity/ActivityItem.tsx
Comment thread apps/web/src/services/api/__tests__/ai-undo-service.test.ts
Comment thread apps/web/src/services/api/__tests__/ai-undo-service.test.ts
Comment thread apps/web/src/services/api/__tests__/ai-undo-service.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/lib/src/monitoring/__tests__/activity-logger-compliance.test.ts (1)

499-625: Comprehensive rollback activity logging tests.

The test suite thoroughly validates logRollbackActivity with excellent coverage of all key scenarios: basic operation logging, value restoration mapping, audit trail preservation, and content snapshots. The comment on line 556 helpfully clarifies the previousValues/newValues mapping semantics.

Optional: Add test for minimal parameters

Consider adding a test case that calls logRollbackActivity without the optional options parameter to explicitly verify it handles minimal parameters gracefully:

it('should handle rollback with minimal parameters', async () => {
  // Arrange & Act
  logRollbackActivity(
    'user-123',
    'source-activity-456',
    {
      resourceType: 'page',
      resourceId: 'page-1',
      driveId: 'drive-1',
    },
    { actorEmail: 'john@example.com' }
    // No options parameter
  );

  // Wait for async execution
  await vi.waitFor(() => {
    expect(capturedInsertValues).not.toBeNull();
  });

  // Assert - verify basic fields are logged without options
  expect(capturedInsertValues).toMatchObject({
    operation: 'rollback',
    resourceType: 'page',
    resourceId: 'page-1',
    driveId: 'drive-1',
    rollbackFromActivityId: 'source-activity-456',
  });
});

This explicitly documents the minimal usage pattern, though the current implementation already handles this safely via optional chaining.

apps/web/src/services/api/ai-undo-service.ts (2)

253-258: Vestigial errors array is unused in success path.

The errors array (line 253) is never populated in the try block after adopting all-or-nothing semantics. Consequently, success: errors.length === 0 (line 342) will always be true in the success path.

Consider simplifying to success: true directly, or removing the errors array from the success result since failures now throw.

🔎 Proposed simplification
     return {
-      success: errors.length === 0,
+      success: true,
       messagesDeleted,
       activitiesRolledBack,
-      errors,
+      errors: [],
     };

Also applies to: 341-346


354-359: Return values may be misleading after transaction rollback.

When the transaction fails and rolls back, activitiesRolledBack (line 357) may contain a non-zero value from iterations before the failure (line 297), even though no changes were actually persisted. This could confuse callers.

Consider resetting counters to 0 in the error path to accurately reflect the rolled-back state:

🔎 Proposed fix
     return {
       success: false,
-      messagesDeleted,
-      activitiesRolledBack,
+      messagesDeleted: 0,
+      activitiesRolledBack: 0,
       errors: [...errors, error instanceof Error ? error.message : 'Unknown error'],
     };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e08706 and a92eaf0.

📒 Files selected for processing (4)
  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/services/api/rollback-service.ts
  • packages/lib/src/monitoring/__tests__/activity-logger-compliance.test.ts
  • packages/lib/src/permissions/rollback-permissions.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/services/api/rollback-service.ts
  • packages/lib/src/permissions/rollback-permissions.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{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
Always use message parts structure for message content: { parts: [{ type: 'text', text: 'content' }] }
Use centralized permissions via import { getUserAccessLevel, canUserEditPage } from '@pagespace/lib/permissions';
Always use Drizzle client from @pagespace/db for database access: import { db, pages } from '@pagespace/db';

Files:

  • apps/web/src/services/api/ai-undo-service.ts
  • packages/lib/src/monitoring/__tests__/activity-logger-compliance.test.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/services/api/ai-undo-service.ts
  • packages/lib/src/monitoring/__tests__/activity-logger-compliance.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/ai-undo-service.ts
  • packages/lib/src/monitoring/__tests__/activity-logger-compliance.test.ts
**/*ai*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vercel AI SDK for AI integrations

Files:

  • apps/web/src/services/api/ai-undo-service.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Register editing state using useEditingStore.getState().startEditing() and .endEditing() to prevent UI refreshes during document editing
Register streaming state using useEditingStore.getState().startStreaming() and .endStreaming() to prevent UI refreshes during AI streaming
Use SWR with isPaused() to prevent refetches during editing, but always allow the initial fetch to complete before pausing

Files:

  • apps/web/src/services/api/ai-undo-service.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`
📚 Learning: 2025-12-23T04:55:07.402Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T04:55:07.402Z
Learning: Applies to packages/db/**/*.{ts,tsx} : Use Drizzle ORM for all database operations in the centralized `packages/db` package

Applied to files:

  • apps/web/src/services/api/ai-undo-service.ts
🧬 Code graph analysis (2)
apps/web/src/services/api/ai-undo-service.ts (4)
packages/db/src/schema/core.ts (2)
  • chatMessages (68-89)
  • pages (24-66)
packages/db/src/schema/conversations.ts (1)
  • messages (30-46)
apps/web/src/services/api/rollback-service.ts (3)
  • RollbackContext (16-16)
  • previewRollback (161-281)
  • executeRollback (287-393)
packages/lib/src/permissions/rollback-permissions.ts (1)
  • RollbackContext (15-19)
packages/lib/src/monitoring/__tests__/activity-logger-compliance.test.ts (1)
packages/lib/src/monitoring/activity-logger.ts (2)
  • logRollbackActivity (644-697)
  • logConversationUndo (704-743)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (7)
packages/lib/src/monitoring/__tests__/activity-logger-compliance.test.ts (2)

39-40: LGTM: New wrapper imports added correctly.

The new convenience wrapper imports are properly integrated into the existing import block.


627-768: Excellent conversation undo logging tests.

The test suite provides comprehensive validation of logConversationUndo with strong coverage of both operation modes, metadata handling, and edge cases. Particularly well done:

  • Tests verify correct operation type mapping based on mode (lines 661-686)
  • Tests validate the semantic meaning of previousValues indicating active messages (lines 718-741)
  • Tests cover the null driveId edge case for global conversations (lines 743-767)
apps/web/src/services/api/ai-undo-service.ts (5)

1-76: LGTM! Well-structured types and imports.

The module header, imports, and type definitions are clean. Imports follow the coding guidelines (Drizzle from @pagespace/db, loggers from @pagespace/lib/server), and all interfaces are properly typed without any any usage.


81-115: LGTM!

The dual-table lookup is a clean abstraction that normalizes messages from both chat_messages and messages tables into a consistent AiMessage shape.


120-126: LGTM!

Efficient helper that selects only the required driveId column.


174-177: Acknowledge the timing edge case.

The comment at lines 175-176 correctly notes that tool calls in the first turn might be missed if there's no preceding message. This is acceptable since user messages typically precede AI responses. The fallback to createdAt is a reasonable default.


272-316: Transaction implementation correctly addresses past review concerns.

The all-or-nothing transaction semantics are properly implemented:

  • All operations (rollbacks + message soft-deletes) are wrapped in a single transaction.
  • Any failure throws to abort the entire transaction.
  • Context determination aligns with preview logic (comment at lines 284-285 explains this).

This addresses the previous review feedback about partial success risks.

- Fix build failure: replace dynamic db.query[] bracket notation with
  explicit conditional to resolve TypeScript union type error
- Extract checkUndoPermissions() helper to eliminate 31 lines of
  duplicated permission logic between GET and POST handlers
- Add existingPreview parameter to executeAiUndo() to avoid redundant
  preview computation (was being called twice per request)
- Simplify context determination: remove redundant isAiGenerated check
  since query already filters for it
- Update tests to match new 4-parameter executeAiUndo signature
- Add clarifying comment explaining partial failure tests document
  defensive handling (actual impl uses all-or-nothing transaction)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (1)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (1)

159-169: Consider simplifying status code logic given all-or-nothing semantics.

With the all-or-nothing transaction semantics in executeAiUndo, partial success is no longer possible—the operation either fully succeeds or fully fails. Once the counter reset issue in ai-undo-service.ts is addressed (see my comment on that file), both messagesDeleted and activitiesRolledBack will always be 0 when success is false, making the 207 status code unreachable.

Consider simplifying to always return 500 on failure:

🔎 Simplified approach
     if (!result.success && result.errors.length > 0) {
-      // Partial success or failure
+      // Operation failed - transaction was rolled back
       return NextResponse.json(
         {
           ...result,
           success: false,
           message: 'Some operations could not be completed',
         },
-        { status: result.messagesDeleted > 0 || result.activitiesRolledBack > 0 ? 207 : 500 }
+        { status: 500 }
       );
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a92eaf0 and a44e4ef.

📒 Files selected for processing (3)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/tests/route.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{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
Always use message parts structure for message content: { parts: [{ type: 'text', text: 'content' }] }
Use centralized permissions via import { getUserAccessLevel, canUserEditPage } from '@pagespace/lib/permissions';
Always use Drizzle client from @pagespace/db for database access: import { db, pages } from '@pagespace/db';

Files:

  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/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/services/api/ai-undo-service.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
**/*ai*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vercel AI SDK for AI integrations

Files:

  • apps/web/src/services/api/ai-undo-service.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Register editing state using useEditingStore.getState().startEditing() and .endEditing() to prevent UI refreshes during document editing
Register streaming state using useEditingStore.getState().startStreaming() and .endStreaming() to prevent UI refreshes during AI streaming
Use SWR with isPaused() to prevent refetches during editing, but always allow the initial fetch to complete before pausing

Files:

  • apps/web/src/services/api/ai-undo-service.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
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, params in dynamic routes are Promise objects. You MUST await context.params before destructuring in route handlers.
Get request body using const body = await request.json();
Get search params using const { searchParams } = new URL(request.url);
Return JSON using Response.json(data) or NextResponse.json(data)

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
🧠 Learnings (4)
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/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/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-23T04:55:07.402Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T04:55:07.402Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permissions via `import { getUserAccessLevel, canUserEditPage } from 'pagespace/lib/permissions';`

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
🧬 Code graph analysis (2)
apps/web/src/services/api/ai-undo-service.ts (2)
packages/lib/src/permissions/rollback-permissions.ts (1)
  • RollbackContext (15-19)
apps/web/src/services/api/rollback-service.ts (3)
  • RollbackContext (16-16)
  • previewRollback (161-281)
  • executeRollback (287-393)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (2)
apps/web/src/services/api/ai-undo-service.ts (3)
  • AiUndoPreview (25-43)
  • previewAiUndo (131-248)
  • executeAiUndo (254-368)
apps/web/src/lib/logging/mask.ts (1)
  • maskIdentifier (1-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (3)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (3)

22-60: Excellent refactoring to eliminate duplication.

The extraction of permission checking logic into a shared helper successfully addresses the previous review feedback about DRY violation. The implementation correctly uses the centralized canUserEditPage function and verifies conversation ownership for global chats.


66-106: GET handler correctly implements Next.js 15 patterns.

The handler properly awaits the params Promise (line 76), uses appropriate authentication options, and leverages the shared permission helper. Error handling and response codes are appropriate.


112-158: POST handler correctly implements authentication, validation, and optimization.

The handler properly:

  • Awaits the params Promise per Next.js 15 requirements (line 122)
  • Uses CSRF-protected authentication for writes (line 118)
  • Validates the request body with Zod (lines 126-132)
  • Passes the preview to executeAiUndo (line 147) to avoid redundant database queries—a good optimization

Comment thread apps/web/src/services/api/ai-undo-service.ts
2witstudios added a commit that referenced this pull request Dec 23, 2025
* feat: add version history and rollback functionality

Implement comprehensive version history browsing and rollback capabilities
for PageSpace, allowing users to restore resources to previous states.

## Schema Changes
- Add 'rollback' operation to activity_operation enum
- Add rollbackFromActivityId and contentFormat fields to activity_logs
- Create retention_policies table for plan-based history retention

## Core Features
- RBAC-based rollback permissions (edit access = rollback access)
- Resource-specific rollback handlers (pages, drives, agents, etc.)
- Plan-based retention limits (7/30/90/unlimited days)
- Rollback creates new activity entry (history never erased)

## API Endpoints
- GET /api/activities/[activityId] - Single activity with rollback eligibility
- POST /api/activities/[activityId]/rollback - Execute rollback
- GET /api/pages/[pageId]/history - Page version history
- GET /api/drives/[driveId]/history - Drive version history (admin)

## UI Components
- VersionHistoryPanel - Slide-out panel with timeline and filters
- VersionHistoryItem - Activity item with "Restore" action
- RollbackConfirmDialog - Confirmation modal with warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: complete rollback handler implementations and fix lint errors

- Fully implement rollbackPermissionChange for grant/revoke/update operations
- Fully implement rollbackMemberChange for add/remove/role change operations
- Fully implement rollbackRoleChange for create/delete/update operations
- Fix lint errors: remove unused imports, fix useEffect dependencies
- Add package exports for @pagespace/lib/permissions and @pagespace/lib/monitoring
- Fix useToast import path to use correct hook location

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add undo AI changes feature for conversations

Add ability to undo AI changes from a specific message point:
- Preview what will be affected before undoing
- Two modes: revert conversation only OR revert with all tool changes
- Activity logging for audit compliance

New files:
- ai-undo-service.ts: preview and execute functions
- UndoAiChangesDialog.tsx: confirmation dialog with mode selection
- /api/ai/chat/messages/[messageId]/undo: GET preview, POST execute

Changes:
- Add conversation_undo operations to activity schema
- Add message rollback handler to rollback-service
- Wire undo button to MessageActionButtons and MessageRenderer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add missing radio-group UI component

Required for UndoAiChangesDialog mode selection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add @radix-ui/react-radio-group dependency

Required for radio-group UI component used in UndoAiChangesDialog.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: restore correct @electron/node-gyp resolution in lockfile

The previous lockfile had a malformed git SSH URL that broke CI:
- Before: git+https://git@github.com:electron/node-gyp.git (broken)
- After: https://codeload.github.com/electron/node-gyp/tar.gz/... (works)

Restored lockfile and re-ran pnpm install to properly resolve dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add critical rules to CLAUDE.md and fix retention tier order

- Add CRITICAL rules for package manager (always pnpm, never npm)
- Add CRITICAL rules for database migrations (never manually edit)
- Fix retention days: founder=90, business=unlimited (was swapped)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(db): add missing default and rollback index

- Add DEFAULT now() for retention_policies.updatedAt column
- Add index on activity_logs.rollbackFromActivityId for queries
- Create migration 0027_fix_retention_updated_at.sql

Addresses PR #118 review issues #10, #20

* fix(lib/permissions): add block scoping and eligibility helper

- Wrap switch case declarations in blocks to prevent variable leakage
- Add isActivityEligibleForRollback() helper for DRY rollback checks
- Export helper for use in history API routes

Addresses PR #118 review issues #5, #13

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(lib/monitoring): add rollback fields to ActivityLogInput

- Add contentFormat field for content type tracking
- Add rollbackFromActivityId for rollback chain references
- Pass fields as top-level properties to logActivity

Addresses PR #118 review issue #3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(api): add authorization and resourceType filtering

- Add permission check before returning activity details
- Implement resourceType query parameter filtering in history routes
- Use shared isActivityEligibleForRollback helper
- Add case-insensitive resource type matching

Addresses PR #118 review issues #6, #7, #13

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(services): improve type safety and add transactions

- Use Drizzle count() instead of .length for efficient queries
- Add operation validation against known valid operations
- Fix unsafe null type assertion in RollbackPreview
- Re-export RollbackContext type for API route usage
- Wrap AI undo mutations in database transaction
- Use switch statement for context determination

Addresses PR #118 review issues #2, #16, #18, #19, #23

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): add CSRF protection and improve error handling

- Use post() with CSRF token for rollback requests
- Add error toast for failed preview fetch
- Handle non-JSON error responses gracefully
- Fix non-existent postWithAuth import to use post
- Remove re-throw after toast notification
- Fix redundant ternary for buttonSize

Addresses PR #118 review issues #1, #8, #11, #17, #22, NEW

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(db): address schema issues from PR #118 review

Critical & Major fixes:
- Add missing .defaultNow() to retention_policies.updatedAt
  (fixes schema/migration mismatch that would cause insert failures)
- Add contentFormatEnum for type-safe content format validation
  (prevents invalid values during rollback parsing)
- Add CHECK constraint: retentionDays >= -1 (where -1 = unlimited)
- Add rollback source snapshot fields for audit trail preservation:
  - rollbackSourceOperation: captures source activity type
  - rollbackSourceTimestamp: captures when source change occurred
  - rollbackSourceTitle: captures resource title at time of change

These denormalized fields survive retention policy deletion, ensuring
complete audit trails even when source activities are purged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(lib): move rollback fields to top-level in activity logger

Fixes Major issue #2 from PR #118 review:
- Move rollbackFromActivityId from metadata to top-level field
- Move contentFormat from metadata to top-level field
- Add rollback source snapshot fields to ActivityLogInput interface
- Update logRollbackActivity to accept and pass snapshot fields

Fields are now stored in dedicated database columns instead of being
nested in the metadata JSONB, enabling proper indexing and querying.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(services): add transaction support to rollback service

Enables atomic rollback operations (Issue #7 from PR #118 review):
- Add optional transaction parameter to executeRollback()
- Update all internal rollback functions to accept database parameter
- Pass rollback source snapshot fields to logRollbackActivity

When a transaction is provided, all database operations use it instead
of the default db connection, enabling atomic rollback + message
deletion in AI undo operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(services): improve atomicity and context logic in AI undo

Fixes Issues #5 and #7 from PR #118 review:

Context determination (#5):
- Align executeAiUndo context logic with previewAiUndo
- Use 'ai_tool' context for pages (all activities here are AI-generated)
- Simplifies from switch statement to direct assignment with drive check

Transaction atomicity (#7):
- Wrap rollbacks AND message deletion in single transaction
- Pass transaction to executeRollback for atomic operations
- If any operation fails, entire undo is rolled back

This ensures users don't end up in inconsistent states where messages
are deleted but only some changes were reverted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(api): use Zod schema for undo route validation

Fixes Issue #6 from PR #118 review:
- Replace manual mode validation with Zod schema
- Aligns with codebase patterns (see rollback route, history route)
- Provides type-safe body parsing with proper TypeScript inference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review feedback

- Add CHECK constraint to retentionPolicies schema definition
  (aligns Drizzle schema with existing migration constraint)
- Change AI undo to all-or-nothing transaction semantics
  (any rollback failure aborts entire operation)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(db): add subscriptionTier enum for retention policies

- Create subscription_tier pgEnum with 'free', 'pro', 'business', 'founder'
- Convert retentionPolicies.subscriptionTier from text to enum
- Adds DB-level validation to prevent invalid tier values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address final CodeRabbit review comments

- Add comments explaining rollbackFromActivityId intentionally lacks FK
  (allows provenance to survive source activity deletion for audit)
- Add TODO note for contentSnapshot storage considerations
- Optimize message fetching: include createdAt in AiUndoPreview to
  avoid double database query

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(db): add USING clause for text-to-enum cast

PostgreSQL requires explicit cast when converting text column to enum type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ai): support undo for global assistant messages by checking both messages tables and updating permission logic

* fix(ai): resolve lint errors in undo route and tests

* test: add contract tests for version history rollback

- Add route tests for /api/activities/[activityId]/rollback (12 tests)
- Add route tests for /api/pages/[pageId]/history (21 tests)
- Add service tests for ai-undo-service (16 tests) with @scaffold label
- Add service tests for rollback-service (28 tests) with @scaffold label
- Add permission tests for rollback-permissions (69 tests)
- Fix undo route test: remove duplicate test expecting wrong status
- Add fake timers to history route tests for deterministic dates

Per rubric v2: service tests use @scaffold labels for ORM chain mocks
pending repository seam refactoring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(ui): add rollback capability to activity views

Add "Restore this version" action to activity items in:
- ActivityDashboard (middle panel for /dashboard/activity and drive activity)
- SidebarActivityTab (right sidebar context-aware activity feed)

Changes:
- ActivityItem: Add hover dropdown menu with rollback action and confirmation dialog
- ActivityTimeline: Pass context and onRollback handler to items
- ActivityDashboard: Handle rollback with proper context mapping (user→user_dashboard, drive→drive)
- SidebarActivityTab: Add rollback UI with context-aware scoping (page/drive/user_dashboard)

Rollback is properly scoped by context to prevent unintended changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add activity logger tests for rollback and undo operations

Add compliance tests for:
- logRollbackActivity: validates rollback operation logging with source activity reference
- logConversationUndo: validates conversation undo logging for both modes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(rollback): support create operation rollback and global messages

- Add 'create' as rollbackable operation (trash resource to undo creation)
- Support both global messages and page chat messages in rollback
- Fix AI undo timing to include tool calls from preceding message
- Handle message create rollback by deactivating the message

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR #118 code review feedback

- Fix build failure: replace dynamic db.query[] bracket notation with
  explicit conditional to resolve TypeScript union type error
- Extract checkUndoPermissions() helper to eliminate 31 lines of
  duplicated permission logic between GET and POST handlers
- Add existingPreview parameter to executeAiUndo() to avoid redundant
  preview computation (was being called twice per request)
- Simplify context determination: remove redundant isAiGenerated check
  since query already filters for it
- Update tests to match new 4-parameter executeAiUndo signature
- Add clarifying comment explaining partial failure tests document
  defensive handling (actual impl uses all-or-nothing transaction)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve header loading and rendering issues across all page types

* style: add truncation for page titles and breadcrumb items

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2witstudios and others added 2 commits December 23, 2025 12:54
Update test expectations to match the new behavior where 'create'
is a rollbackable operation (rolling back a create = trashing the resource).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When executeAiUndo catches an error, the transaction has been rolled
back so no changes were committed. Reset messagesDeleted and
activitiesRolledBack to 0 to accurately reflect this.

Also simplify the route handler to always return 500 on failure since
partial success (207) is now unreachable with all-or-nothing semantics.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/lib/src/permissions/__tests__/rollback-permissions.test.ts (1)

110-126: Consider removing unnecessary type assertion for valid types.

Line 115 uses as any for types that should already be valid ActivityResourceType values. This weakens type safety unnecessarily.

Line 123's as any is acceptable for testing runtime behavior with invalid inputs, but could be documented with a comment explaining the intent.

🔎 Proposed improvement
     it.each(rollbackableTypes)('returns true for %s resource type', (type) => {
-      expect(isRollbackableResourceType(type as any)).toBe(true);
+      expect(isRollbackableResourceType(type as ActivityResourceType)).toBe(true);
     });
   });

   describe('non-rollbackable resource types', () => {
     const nonRollbackableTypes = ['user', 'file', 'token', 'device', 'conversation'];

     it.each(nonRollbackableTypes)('returns false for %s resource type', (type) => {
+      // Testing runtime behavior with invalid types
       expect(isRollbackableResourceType(type as any)).toBe(false);
     });

As per coding guidelines, avoid using any types where possible.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a44e4ef and b365244.

📒 Files selected for processing (1)
  • packages/lib/src/permissions/__tests__/rollback-permissions.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{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/__tests__/rollback-permissions.test.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/__tests__/rollback-permissions.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/permissions/__tests__/rollback-permissions.test.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`
🧬 Code graph analysis (1)
packages/lib/src/permissions/__tests__/rollback-permissions.test.ts (1)
packages/lib/src/permissions/rollback-permissions.ts (4)
  • ActivityForPermissionCheck (24-33)
  • isRollbackableOperation (167-188)
  • isRollbackableResourceType (151-162)
  • isActivityEligibleForRollback (195-204)
🔇 Additional comments (6)
packages/lib/src/permissions/__tests__/rollback-permissions.test.ts (6)

1-31: LGTM! Well-structured test setup.

The imports, mocking setup, and test fixtures are properly organized. The mock is correctly placed before the imports that depend on it, and the fixture data provides clear test values.


32-42: LGTM! Excellent test fixture pattern.

The factory function provides type-safe activity creation with sensible defaults and flexible overrides.


44-47: LGTM! Proper test isolation.

The beforeEach hook correctly clears mocks between tests.


53-108: LGTM! Comprehensive operation coverage.

The tests cover all rollbackable and non-rollbackable operations, including the 'create' operation mentioned in the commit message. The inline comment on line 94 helpfully documents the rollback prevention logic.


128-188: LGTM! Thorough eligibility checks.

The tests comprehensively cover all eligibility conditions, including the critical check that prevents rolling back a rollback operation (lines 179-187), which prevents infinite rollback chains.


194-369: LGTM! Comprehensive permission coverage.

The tests thoroughly cover all rollback contexts (AI tool, page, drive, user dashboard, unknown) with proper verification of both success and failure cases. The tests correctly:

  • Verify permission checks are called with expected arguments
  • Test missing identifier scenarios (pageId, driveId)
  • Validate both canRollback boolean and reason messages
  • Prevent rollback-of-rollback infinite chains

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (1)

159-168: Error message misleading given all-or-nothing transaction semantics.

Since executeAiUndo now uses all-or-nothing transaction semantics (verified in ai-undo-service.ts lines 281-323), when result.success is false, the entire operation failed and was rolled back—no partial changes were applied. The message "Some operations could not be completed" incorrectly suggests partial success.

🔎 Proposed fix to clarify failure message
     if (!result.success && result.errors.length > 0) {
       return NextResponse.json(
         {
           ...result,
           success: false,
-          message: 'Some operations could not be completed',
+          message: 'Operation failed',
         },
         { status: 500 }
       );
     }

Additionally, the condition at line 159 can be simplified since errors.length > 0 is always true when success is false:

-    if (!result.success && result.errors.length > 0) {
+    if (!result.success) {
       return NextResponse.json(
         {
           ...result,
           success: false,
           message: 'Operation failed',
         },
         { status: 500 }
       );
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b365244 and 528cf5d.

📒 Files selected for processing (2)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
🧰 Additional context used
📓 Path-based instructions (6)
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/ai/chat/messages/[messageId]/undo/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/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.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/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.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/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/services/api/ai-undo-service.ts
**/*ai*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vercel AI SDK for AI integrations

Files:

  • apps/web/src/services/api/ai-undo-service.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Tech stack: Next.js 15 App Router + TypeScript + Tailwind + shadcn/ui (frontend), PostgreSQL + Drizzle ORM (database), Ollama + Vercel AI SDK + OpenRouter + Google AI SDK (AI), custom JWT auth, local filesystem storage, Socket.IO for real-time, Docker deployment
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-23T18:49:41.955Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.955Z
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/ai/chat/messages/[messageId]/undo/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/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
🧬 Code graph analysis (1)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (1)
apps/web/src/services/api/ai-undo-service.ts (3)
  • AiUndoPreview (25-43)
  • previewAiUndo (131-248)
  • executeAiUndo (254-368)
🔇 Additional comments (4)
apps/web/src/services/api/ai-undo-service.ts (1)

1-368: LGTM! All previous issues have been successfully addressed.

The service implementation is well-structured with:

  • Proper all-or-nothing transaction semantics (lines 281-323) with throws on failure
  • Consistent context determination between preview and execute functions
  • Correct counter reset to 0 in the catch block (lines 363-364)
  • Clean separation of concerns with helper functions
  • Comprehensive error handling and logging

The code follows TypeScript best practices and integrates properly with Drizzle ORM and the rollback service.

apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (3)

1-60: LGTM! Excellent implementation of authentication and permission checking.

The code properly:

  • Requires CSRF protection for write operations (line 16)
  • Uses centralized canUserEditPage permission function (line 32)
  • Eliminates duplication with the checkUndoPermissions helper
  • Follows Next.js 15 conventions

66-106: LGTM! GET handler follows Next.js 15 best practices.

The preview endpoint correctly:

  • Awaits context.params per Next.js 15 requirements (line 76)
  • Uses read-only authentication without CSRF
  • Checks permissions before returning preview
  • Handles errors appropriately

112-186: POST handler implementation is solid.

The execution endpoint correctly:

  • Requires CSRF protection via AUTH_OPTIONS_WRITE
  • Awaits context.params per Next.js 15 requirements (line 122)
  • Validates request body with Zod
  • Reuses preview to avoid redundant computation (line 147)
  • Returns appropriate success messages based on mode

2witstudios and others added 4 commits December 23, 2025 13:01
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- useBreadcrumbs: Fix isLoading expectation for null pageId
  When pageId is null, isLoading correctly returns false (nothing to load)

- usePermissions: Fix SWR mock to trigger onSuccess callback
  isPaused requires hasLoadedRef.current=true, which is set by onSuccess

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
apps/web/src/hooks/__tests__/usePermissions.test.ts (1)

99-120: Enhance test coverage for the "not editing after load" scenario.

The test currently validates isPaused() returns false before the initial load completes (since onSuccess isn't called, hasLoadedRef.current remains false). However, it doesn't test the intended scenario described in the test name: after initial load completes, when not editing, isPaused should return false.

For comprehensive coverage, this test should also trigger onSuccess (like tests 1 and 2) to set hasLoadedRef.current = true, then verify isPaused() returns false when isAnyActive = false. This would properly test the logic: hasLoadedRef.current && isAnyActive → true && false = false.

🔎 Suggested enhancement
    it('given user is not editing or streaming, should allow permission revalidation', () => {
      // Arrange: No active editing/streaming
      vi.mocked(useEditingStore).mockReturnValue(false); // isAnyActive returns false

-      vi.mocked(useSWR).mockReturnValue({
-        data: undefined,
-        error: undefined,
-        isLoading: false,
-        mutate: vi.fn(),
-        isValidating: false,
-      } as SWRResponse);
+      // Mock SWR and trigger onSuccess to set hasLoadedRef.current = true
+      vi.mocked(useSWR).mockImplementation((key, fetcher, config) => {
+        // Trigger onSuccess to simulate initial load completed
+        if (config?.onSuccess) {
+          config.onSuccess({ canView: true, canEdit: true, canShare: true, canDelete: true }, key as string, {} as never);
+        }
+        return {
+          data: { canView: true, canEdit: true, canShare: true, canDelete: true },
+          error: undefined,
+          isLoading: false,
+          mutate: vi.fn(),
+          isValidating: false,
+        } as SWRResponse;
+      });

      // Act: Render hook
      renderHook(() => usePermissions('page-123'));

-      // Assert: SWR isPaused returns false
+      // Assert: SWR isPaused returns false (after initial load, when not editing)
      const swrCall = vi.mocked(useSWR).mock.calls[0];
      const swrConfig = swrCall[2] as { isPaused?: () => boolean };

      expect(swrConfig.isPaused).toBeDefined();
      expect(swrConfig.isPaused!()).toBe(false);
    });
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts (1)

349-365: Test updates align with standardized error handling.

The test correctly validates the new 500 response with the "Undo failed. No changes were applied." message.

Consider adding a test case for the edge scenario where executeAiUndo returns { success: false, errors: [] } to ensure it's handled correctly (this is especially relevant given the condition on route.ts line 159).

🔎 Optional additional test
it('returns 500 when operation fails without error details', async () => {
  (executeAiUndo as Mock).mockResolvedValue({
    success: false,
    messagesDeleted: 0,
    activitiesRolledBack: 0,
    errors: [],
  });

  const response = await POST(createPostRequest({ mode: 'messages_only' }), { params: mockParams });
  const body = await response.json();

  expect(response.status).toBe(500);
  expect(body.success).toBe(false);
});
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 528cf5d and e8e5a20.

📒 Files selected for processing (5)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/components/layout/middle-content/CenterPanel.tsx
  • apps/web/src/hooks/__tests__/useBreadcrumbs.test.ts
  • apps/web/src/hooks/__tests__/usePermissions.test.ts
💤 Files with no reviewable changes (1)
  • apps/web/src/components/layout/middle-content/CenterPanel.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{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/hooks/__tests__/useBreadcrumbs.test.ts
  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.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/hooks/__tests__/useBreadcrumbs.test.ts
  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/hooks/__tests__/useBreadcrumbs.test.ts
  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.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/hooks/__tests__/useBreadcrumbs.test.ts
  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
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/ai/chat/messages/[messageId]/undo/route.ts
🧠 Learnings (6)
📚 Learning: 2025-12-23T18:49:41.955Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.955Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : For SWR data fetching with editing protection, use `isPaused: () => hasLoadedRef.current && isEditingActive()` to allow initial fetch and only pause after, with `revalidateOnFocus: false`

Applied to files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/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/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-23T18:49:41.955Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.955Z
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/hooks/__tests__/usePermissions.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-23T18:49:41.955Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.955Z
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/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
🧬 Code graph analysis (2)
apps/web/src/hooks/__tests__/useBreadcrumbs.test.ts (1)
apps/web/src/hooks/useBreadcrumbs.ts (1)
  • useBreadcrumbs (23-50)
apps/web/src/hooks/__tests__/usePermissions.test.ts (1)
apps/web/src/hooks/usePermissions.ts (1)
  • usePermissions (33-98)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (2)
apps/web/src/hooks/__tests__/usePermissions.test.ts (1)

41-86: LGTM! Proper simulation of SWR load lifecycle.

The mockImplementation approach correctly simulates the initial load completion by invoking the onSuccess callback, which sets hasLoadedRef.current = true in the actual hook. This allows the tests to properly validate that isPaused returns true when editing is active after the initial load, matching the intended behavior from the hook implementation.

apps/web/src/hooks/__tests__/useBreadcrumbs.test.ts (1)

175-180: LGTM! Test correctly validates null pageId behavior.

The updated assertion and descriptive test name accurately reflect the hook's implementation, where isLoading evaluates to false when pageId is null (!error && !data && !!pageId = false). The inline comment clearly documents the boolean logic evaluation.

Comment thread apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts Outdated
- Remove `&& result.errors.length > 0` condition so any success:false
  triggers 500 response, not just failures with non-empty errors
- Add test for empty errors array edge case
- Enhance usePermissions test to properly validate after onSuccess

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
apps/web/src/hooks/__tests__/usePermissions.test.ts (1)

45-45: Consider using a more specific type cast for test clarity.

The {} as never cast for the third parameter to onSuccess works but could be more explicit. Consider using {} as any or defining a minimal mock config object for better readability.

🔎 Alternative approach
-          config.onSuccess({ canView: true, canEdit: true, canShare: true, canDelete: true }, key as string, {} as never);
+          config.onSuccess({ canView: true, canEdit: true, canShare: true, canDelete: true }, key as string, {} as any);

Or define a reusable mock config:

const mockSWRConfig = {} as SWRConfiguration;
// Then use: config.onSuccess(..., key as string, mockSWRConfig);

Also applies to: 77-77, 108-108

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8e5a20 and cdcf908.

📒 Files selected for processing (3)
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
  • apps/web/src/hooks/__tests__/usePermissions.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{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/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/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/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/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/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
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/ai/chat/messages/[messageId]/undo/route.ts
🧠 Learnings (6)
📚 Learning: 2025-12-23T18:49:41.955Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.955Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : For SWR data fetching with editing protection, use `isPaused: () => hasLoadedRef.current && isEditingActive()` to allow initial fetch and only pause after, with `revalidateOnFocus: false`

Applied to files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
📚 Learning: 2025-12-22T20:04:40.892Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.892Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/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/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-23T18:49:41.955Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.955Z
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/hooks/__tests__/usePermissions.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
📚 Learning: 2025-12-23T18:49:41.955Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.955Z
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/hooks/__tests__/usePermissions.test.ts
  • apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts
🧬 Code graph analysis (2)
apps/web/src/hooks/__tests__/usePermissions.test.ts (1)
apps/web/src/hooks/usePermissions.ts (1)
  • usePermissions (33-98)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts (3)
apps/web/src/services/api/ai-undo-service.ts (1)
  • executeAiUndo (254-368)
apps/web/src/services/api/index.ts (1)
  • executeAiUndo (61-61)
apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (1)
  • POST (112-186)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (5)
apps/web/src/hooks/__tests__/usePermissions.test.ts (3)

37-67: LGTM! Test correctly simulates initial load and verifies isPaused behavior.

The mock implementation properly triggers the onSuccess callback to set hasLoadedRef.current = true, then correctly verifies that isPaused() returns true when editing is active. This aligns with the learned pattern for SWR editing protection.


69-97: LGTM! Correct verification of isPaused during AI streaming.

The test correctly simulates the initial load completion and verifies that permission revalidation is paused when AI streaming is active, matching the expected behavior from the hook implementation.


99-127: LGTM! Correctly validates revalidation when no editing activity.

The test properly simulates the loaded state and verifies that isPaused() returns false when no editing or streaming is active. The comment on line 121 accurately describes the logic: hasLoadedRef.current=true && isAnyActive=false => false.

apps/web/src/app/api/ai/chat/messages/[messageId]/undo/route.ts (1)

159-168: LGTM! Critical logic gap fixed.

The simplified failure condition now correctly catches all failure cases, including when success: false with an empty errors array. This prevents failures from falling through to the success response path and aligns with the all-or-nothing transaction semantics in executeAiUndo.

The standardized failure response (message: 'Undo failed. No changes were applied.', status: 500) provides a clear, consistent API contract.

apps/web/src/app/api/ai/chat/messages/[messageId]/undo/__tests__/route.test.ts (1)

349-381: LGTM! Comprehensive failure test coverage.

The test suite now correctly validates both failure scenarios:

  1. Failures with error details (errors: ['Complete failure'])
  2. Failures with empty errors array (defensive edge case)

The mock values (messagesDeleted: 0, activitiesRolledBack: 0) accurately reflect the all-or-nothing transaction behavior in executeAiUndo, where any failure aborts the entire operation before counters are incremented.

Both tests verify the standardized failure response: status: 500 and message: 'Undo failed. No changes were applied.'

@2witstudios
2witstudios merged commit 1e490ae into master Dec 23, 2025
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Dec 23, 2025
2 of 4 tasks
2witstudios added a commit that referenced this pull request Dec 24, 2025
* feat: add version history and rollback functionality

Implement comprehensive version history browsing and rollback capabilities
for PageSpace, allowing users to restore resources to previous states.

## Schema Changes
- Add 'rollback' operation to activity_operation enum
- Add rollbackFromActivityId and contentFormat fields to activity_logs
- Create retention_policies table for plan-based history retention

## Core Features
- RBAC-based rollback permissions (edit access = rollback access)
- Resource-specific rollback handlers (pages, drives, agents, etc.)
- Plan-based retention limits (7/30/90/unlimited days)
- Rollback creates new activity entry (history never erased)

## API Endpoints
- GET /api/activities/[activityId] - Single activity with rollback eligibility
- POST /api/activities/[activityId]/rollback - Execute rollback
- GET /api/pages/[pageId]/history - Page version history
- GET /api/drives/[driveId]/history - Drive version history (admin)

## UI Components
- VersionHistoryPanel - Slide-out panel with timeline and filters
- VersionHistoryItem - Activity item with "Restore" action
- RollbackConfirmDialog - Confirmation modal with warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: complete rollback handler implementations and fix lint errors

- Fully implement rollbackPermissionChange for grant/revoke/update operations
- Fully implement rollbackMemberChange for add/remove/role change operations
- Fully implement rollbackRoleChange for create/delete/update operations
- Fix lint errors: remove unused imports, fix useEffect dependencies
- Add package exports for @pagespace/lib/permissions and @pagespace/lib/monitoring
- Fix useToast import path to use correct hook location

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: add undo AI changes feature for conversations

Add ability to undo AI changes from a specific message point:
- Preview what will be affected before undoing
- Two modes: revert conversation only OR revert with all tool changes
- Activity logging for audit compliance

New files:
- ai-undo-service.ts: preview and execute functions
- UndoAiChangesDialog.tsx: confirmation dialog with mode selection
- /api/ai/chat/messages/[messageId]/undo: GET preview, POST execute

Changes:
- Add conversation_undo operations to activity schema
- Add message rollback handler to rollback-service
- Wire undo button to MessageActionButtons and MessageRenderer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add missing radio-group UI component

Required for UndoAiChangesDialog mode selection.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add @radix-ui/react-radio-group dependency

Required for radio-group UI component used in UndoAiChangesDialog.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: restore correct @electron/node-gyp resolution in lockfile

The previous lockfile had a malformed git SSH URL that broke CI:
- Before: git+https://git@github.com:electron/node-gyp.git (broken)
- After: https://codeload.github.com/electron/node-gyp/tar.gz/... (works)

Restored lockfile and re-ran pnpm install to properly resolve dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add critical rules to CLAUDE.md and fix retention tier order

- Add CRITICAL rules for package manager (always pnpm, never npm)
- Add CRITICAL rules for database migrations (never manually edit)
- Fix retention days: founder=90, business=unlimited (was swapped)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(db): add missing default and rollback index

- Add DEFAULT now() for retention_policies.updatedAt column
- Add index on activity_logs.rollbackFromActivityId for queries
- Create migration 0027_fix_retention_updated_at.sql

Addresses PR #118 review issues #10, #20

* fix(lib/permissions): add block scoping and eligibility helper

- Wrap switch case declarations in blocks to prevent variable leakage
- Add isActivityEligibleForRollback() helper for DRY rollback checks
- Export helper for use in history API routes

Addresses PR #118 review issues #5, #13

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(lib/monitoring): add rollback fields to ActivityLogInput

- Add contentFormat field for content type tracking
- Add rollbackFromActivityId for rollback chain references
- Pass fields as top-level properties to logActivity

Addresses PR #118 review issue #3

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(api): add authorization and resourceType filtering

- Add permission check before returning activity details
- Implement resourceType query parameter filtering in history routes
- Use shared isActivityEligibleForRollback helper
- Add case-insensitive resource type matching

Addresses PR #118 review issues #6, #7, #13

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(services): improve type safety and add transactions

- Use Drizzle count() instead of .length for efficient queries
- Add operation validation against known valid operations
- Fix unsafe null type assertion in RollbackPreview
- Re-export RollbackContext type for API route usage
- Wrap AI undo mutations in database transaction
- Use switch statement for context determination

Addresses PR #118 review issues #2, #16, #18, #19, #23

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): add CSRF protection and improve error handling

- Use post() with CSRF token for rollback requests
- Add error toast for failed preview fetch
- Handle non-JSON error responses gracefully
- Fix non-existent postWithAuth import to use post
- Remove re-throw after toast notification
- Fix redundant ternary for buttonSize

Addresses PR #118 review issues #1, #8, #11, #17, #22, NEW

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(db): address schema issues from PR #118 review

Critical & Major fixes:
- Add missing .defaultNow() to retention_policies.updatedAt
  (fixes schema/migration mismatch that would cause insert failures)
- Add contentFormatEnum for type-safe content format validation
  (prevents invalid values during rollback parsing)
- Add CHECK constraint: retentionDays >= -1 (where -1 = unlimited)
- Add rollback source snapshot fields for audit trail preservation:
  - rollbackSourceOperation: captures source activity type
  - rollbackSourceTimestamp: captures when source change occurred
  - rollbackSourceTitle: captures resource title at time of change

These denormalized fields survive retention policy deletion, ensuring
complete audit trails even when source activities are purged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(lib): move rollback fields to top-level in activity logger

Fixes Major issue #2 from PR #118 review:
- Move rollbackFromActivityId from metadata to top-level field
- Move contentFormat from metadata to top-level field
- Add rollback source snapshot fields to ActivityLogInput interface
- Update logRollbackActivity to accept and pass snapshot fields

Fields are now stored in dedicated database columns instead of being
nested in the metadata JSONB, enabling proper indexing and querying.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(services): add transaction support to rollback service

Enables atomic rollback operations (Issue #7 from PR #118 review):
- Add optional transaction parameter to executeRollback()
- Update all internal rollback functions to accept database parameter
- Pass rollback source snapshot fields to logRollbackActivity

When a transaction is provided, all database operations use it instead
of the default db connection, enabling atomic rollback + message
deletion in AI undo operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(services): improve atomicity and context logic in AI undo

Fixes Issues #5 and #7 from PR #118 review:

Context determination (#5):
- Align executeAiUndo context logic with previewAiUndo
- Use 'ai_tool' context for pages (all activities here are AI-generated)
- Simplifies from switch statement to direct assignment with drive check

Transaction atomicity (#7):
- Wrap rollbacks AND message deletion in single transaction
- Pass transaction to executeRollback for atomic operations
- If any operation fails, entire undo is rolled back

This ensures users don't end up in inconsistent states where messages
are deleted but only some changes were reverted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(api): use Zod schema for undo route validation

Fixes Issue #6 from PR #118 review:
- Replace manual mode validation with Zod schema
- Aligns with codebase patterns (see rollback route, history route)
- Provides type-safe body parsing with proper TypeScript inference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review feedback

- Add CHECK constraint to retentionPolicies schema definition
  (aligns Drizzle schema with existing migration constraint)
- Change AI undo to all-or-nothing transaction semantics
  (any rollback failure aborts entire operation)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(db): add subscriptionTier enum for retention policies

- Create subscription_tier pgEnum with 'free', 'pro', 'business', 'founder'
- Convert retentionPolicies.subscriptionTier from text to enum
- Adds DB-level validation to prevent invalid tier values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address final CodeRabbit review comments

- Add comments explaining rollbackFromActivityId intentionally lacks FK
  (allows provenance to survive source activity deletion for audit)
- Add TODO note for contentSnapshot storage considerations
- Optimize message fetching: include createdAt in AiUndoPreview to
  avoid double database query

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(db): add USING clause for text-to-enum cast

PostgreSQL requires explicit cast when converting text column to enum type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ai): support undo for global assistant messages by checking both messages tables and updating permission logic

* fix(ai): resolve lint errors in undo route and tests

* test: add contract tests for version history rollback

- Add route tests for /api/activities/[activityId]/rollback (12 tests)
- Add route tests for /api/pages/[pageId]/history (21 tests)
- Add service tests for ai-undo-service (16 tests) with @scaffold label
- Add service tests for rollback-service (28 tests) with @scaffold label
- Add permission tests for rollback-permissions (69 tests)
- Fix undo route test: remove duplicate test expecting wrong status
- Add fake timers to history route tests for deterministic dates

Per rubric v2: service tests use @scaffold labels for ORM chain mocks
pending repository seam refactoring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(ui): add rollback capability to activity views

Add "Restore this version" action to activity items in:
- ActivityDashboard (middle panel for /dashboard/activity and drive activity)
- SidebarActivityTab (right sidebar context-aware activity feed)

Changes:
- ActivityItem: Add hover dropdown menu with rollback action and confirmation dialog
- ActivityTimeline: Pass context and onRollback handler to items
- ActivityDashboard: Handle rollback with proper context mapping (user→user_dashboard, drive→drive)
- SidebarActivityTab: Add rollback UI with context-aware scoping (page/drive/user_dashboard)

Rollback is properly scoped by context to prevent unintended changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add activity logger tests for rollback and undo operations

Add compliance tests for:
- logRollbackActivity: validates rollback operation logging with source activity reference
- logConversationUndo: validates conversation undo logging for both modes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(rollback): support create operation rollback and global messages

- Add 'create' as rollbackable operation (trash resource to undo creation)
- Support both global messages and page chat messages in rollback
- Fix AI undo timing to include tool calls from preceding message
- Handle message create rollback by deactivating the message

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address PR #118 code review feedback

- Fix build failure: replace dynamic db.query[] bracket notation with
  explicit conditional to resolve TypeScript union type error
- Extract checkUndoPermissions() helper to eliminate 31 lines of
  duplicated permission logic between GET and POST handlers
- Add existingPreview parameter to executeAiUndo() to avoid redundant
  preview computation (was being called twice per request)
- Simplify context determination: remove redundant isAiGenerated check
  since query already filters for it
- Update tests to match new 4-parameter executeAiUndo signature
- Add clarifying comment explaining partial failure tests document
  defensive handling (actual impl uses all-or-nothing transaction)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: update rollback-permissions tests for create operation

Update test expectations to match the new behavior where 'create'
is a rollbackable operation (rolling back a create = trashing the resource).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: reset undo counters on transaction failure

When executeAiUndo catches an error, the transaction has been rolled
back so no changes were committed. Reset messagesDeleted and
activitiesRolledBack to 0 to accurately reflect this.

Also simplify the route handler to always return 500 on failure since
partial success (207) is now unreachable with all-or-nothing semantics.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: remove unused params variable in OptimizedViewHeader

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(web): refine AI undo error message to reflect atomic transaction semantics

* fix(tests): correct hook test expectations for SWR behavior

- useBreadcrumbs: Fix isLoading expectation for null pageId
  When pageId is null, isLoading correctly returns false (nothing to load)

- usePermissions: Fix SWR mock to trigger onSuccess callback
  isPaused requires hasLoadedRef.current=true, which is set by onSuccess

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: handle all failure cases in AI undo route

- Remove `&& result.errors.length > 0` condition so any success:false
  triggers 500 response, not just failures with non-empty errors
- Add test for empty errors array edge case
- Enhance usePermissions test to properly validate after onSuccess

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: resolve AI chat undo feature not working

The undo feature was always showing "Failed to undo changes" due to incorrect
usage of the post() helper from auth-fetch. The post() function:
- Returns parsed JSON on success (not a Response object)
- Throws an error on non-2xx responses

Components were incorrectly checking res.ok (undefined on parsed JSON) and
calling res.json() on already-parsed objects, causing the error branch to
always trigger.

Changes:
- Fix UndoAiChangesDialog to properly use post() - await and catch errors
- Fix ActivityDashboard, SidebarActivityTab, VersionHistoryPanel (same pattern)
- Update page-write-tools activity logging to pass previousValues for rollback
  support (replace_lines, rename_page, trash/restore, move_page, edit_sheet_cells
  now store original state for proper undo)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
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