Skip to content

feat: add excluded users setting and recalculate feature#100

Merged
coji merged 6 commits intomainfrom
fix/exclude-bot-users
Dec 24, 2025
Merged

feat: add excluded users setting and recalculate feature#100
coji merged 6 commits intomainfrom
fix/exclude-bot-users

Conversation

@coji
Copy link
Owner

@coji coji commented Dec 16, 2025

Summary

  • Add organization-specific excluded users setting to filter bot users from cycle time calculations
  • Add recalculate feature in admin settings to re-analyze PRs with updated settings
  • Add Claude Code formatting hook for auto-prettier on file edits
  • Add authentication to recalculate API endpoint
  • Simplify data directory resolution (remove UPFLOW_DATA_DIR requirement)

Background

GitHub API returns Copilot for discussion comments (without [bot] suffix), causing AI comments to be counted as human reviews in pickup time calculations. This fix:

  1. Adds default exclusion for Copilot
  2. Allows organizations to configure additional excluded users (e.g., iris-tech-bot)
  3. Provides admin UI to trigger recalculation after settings change

Changes

Excluded Users Feature

  • Added excludedUsers column to organization_settings table
  • Modified buildPullRequests to filter out excluded users from cycle time calculations
  • Added UI in admin settings for configuring excluded users

Recalculate Feature

  • SSE API endpoint (/api/admin/recalculate/{organizationId}) for real-time progress
  • UI component with progress bar showing repo-by-repo progress
  • Analyze-only mode (no re-fetch from GitHub API)
  • Added requireSuperAdmin authentication check

Data Directory Resolution

  • Removed UPFLOW_DATA_DIR environment variable requirement
  • Simplified to default ./data path (consistent with project structure)
  • UPFLOW_DATA_DIR still supported for custom deployments

Developer Experience

  • Added Claude Code hook for auto-formatting with prettier

Test plan

  • Verify excluded users setting saves correctly
  • Verify recalculate shows progress and completes successfully
  • Verify pickup times are recalculated excluding specified users
  • Verify authentication is required for recalculate endpoint

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Recalculate workflow on Organization Settings with live progress, status, and streaming updates.
    • Excluded Users setting and form field to filter specified users from PR reviews and discussions.
  • Documentation

    • Tech Stack updated to reflect new UI, testing, and formatting tools.
  • Chores

    • Automatic code formatting on edit.
    • Database migration adding excludedUsers to organization settings.
    • CLI/path helper cleanup and related test updates.

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

coji and others added 3 commits December 17, 2025 00:00
- Add excludedUsers column to organization_settings table
- Exclude 'Copilot' by default (GitHub API records it without [bot] suffix in discussions)
- Add UI in admin settings to configure additional excluded users per organization
- Update batch processing to filter out excluded users from reviews/discussions

This fixes incorrect pickup time calculations caused by Copilot comments
being counted as human reviews.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SSE API endpoint for recalculating cycle times with progress updates
- Add Recalculate UI component with progress bar
- Extend analyze function to support progress callback
- Integrate recalculate feature into organization settings page

This allows administrators to trigger cycle time recalculation
from the admin UI after changing excluded users settings.

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

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

coderabbitai bot commented Dec 16, 2025

📝 Walkthrough

Walkthrough

Adds an SSE-driven organization "recalculate" feature with UI and API, persists a new excludedUsers setting (DB migration, schema, types), propagates exclusions into batch GitHub analysis with per-repo progress, adds an auto-format hook, and adjusts path helpers, package deps, and tests.

Changes

Cohort / File(s) Change Summary
Auto-formatting Hook
\.claude/hooks/format-on-edit.ts
New PostToolUse hook that runs pnpm exec prettier --write for edited files with extensions .ts/.tsx/.js/.jsx/.json/.md/.css; logs errors and always returns success.
Docs / Manifest
CLAUDE.md, package.json
Updated tech stack and spacing in CLAUDE.md; moved tsx to devDependencies and added cc-hooks-ts to deps/devDeps in package.json.
Schema & Migration
prisma/schema.prisma, prisma/migrations/..._add_excluded_users_to_organization_settings/migration.sql, prisma/migrations/migration_lock.toml
Added excludedUsers string (default "") to OrganizationSetting and corresponding SQLite migration; minor lockfile newline adjustment.
Types & Models
app/services/type.ts, batch/provider/github/model.ts
Added excludedUsers to OrganizationSetting type; allowed GitHub review comment user login to be nullable.
DB Queries (batch)
batch/db/queries.ts
Included organizationSettings.excludedUsers in listAllOrganizations and getOrganization selections.
Batch Provider — GitHub
batch/provider/github/provider.ts, batch/provider/github/pullrequest.ts
analyze() signature extended to accept excludedUsers and optional onProgress callback; provider reports per-repo progress via onProgress and forwards excludedUsers into buildPullRequests; buildPullRequests parses comma-separated exclusions, filters reviews/discussions accordingly, and tolerates null authors.
CLI / Path Helpers & Tests
batch/cli.ts, batch/helper/path-builder.ts, batch/helper/path.ts, batch/helper/path-builder.test.ts
Removed runtime UPFLOW_DATA_DIR init from CLI; added exported getDataDir() and changed JSON dir resolution to use it; updated tests to assert getDataDir() and new base paths.
Organization Settings — Types & Form
app/routes/admin+/$organization.settings/types.ts, app/routes/admin+/$organization.settings/forms/organization-settings.tsx
Added excludedUsers (z.string().default('')) to settings schema and added corresponding excludedUsers field to the settings form with default value and helper UI.
Organization Settings — Actions & Mutations
app/routes/admin+/$organization.settings/forms/organization-settings.action.server.ts, app/routes/admin+/$organization.settings/functions/mutations.server.ts, app/routes/admin+/$organization.settings/forms/index.ts
Form action now calls updateOrganization and new updateOrganizationSetting; added updateOrganizationSetting export to update organization settings (including excludedUsers); re-exported ./recalculate from forms index.
Recalculate UI & Integration
app/routes/admin+/$organization.settings/forms/recalculate.tsx, app/routes/admin+/$organization.settings/route.tsx
New Recalculate React component opens SSE to /api/admin/recalculate/{org}, tracks status/progress/messages, and is rendered on organization settings page.
SSE Recalculate API
app/routes/api.admin.recalculate.$organization.ts
New SSE loader with per-organization concurrency (runningJobs), super-admin auth, streams events (start/progress/upsert/export/complete/error), calls provider analyze() with progress callback, upserts PullRequests, optionally exports spreadsheets, and ensures cleanup.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as Recalculate UI
    participant SSE as /api/admin/recalculate
    participant Provider as GitHub Provider
    participant DB as Database

    User->>UI: Click "Recalculate"
    UI->>SSE: Open SSE connection (GET)
    SSE->>SSE: Check/Add runningJobs lock
    SSE->>DB: Load organization & settings
    SSE->>Provider: analyze(orgSetting, onProgress)
    Provider->>Provider: For each repo -> onProgress(repo, current, total)
    Provider->>SSE: Return shaped PullRequests
    SSE->>DB: Upsert PullRequests
    SSE->>UI: stream "progress"/"upsert" events
    alt export configured
        SSE->>DB: Export pulls & reviews to sheets
        SSE->>UI: stream "export" event
    end
    SSE->>UI: stream "complete"
    SSE->>SSE: Release runningJobs lock & close
    alt error
        SSE->>UI: stream "error"
        SSE->>SSE: Release runningJobs lock & close
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped to add a prettier sweep,

SSE hums while I count each repo,
Excluded names tucked neat and deep,
Recalculate sings as numbers grow,
A tidy run — nibble, hop, and go! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main features added: an excludedUsers setting and a recalculate feature, which directly match the key changes across the changeset.
✨ 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 fix/exclude-bot-users

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (5)
batch/provider/github/model.ts (1)

51-51: Consider using NonNullable for type consistency.

The user field definition differs from similar fields in this file. Lines 44 and 58 use NonNullable<...>['login'] | null pattern, which safely handles cases where the user object itself might be null in the API response.

Apply this diff for consistency and type safety:

-  user: GitHubReviewComment['user']['login'] | null
+  user: NonNullable<GitHubReviewComment['user']>['login'] | null
app/routes/admin+/$organization.settings/types.ts (1)

11-11: Consider adding validation or format documentation for the user list.

The excludedUsers field accepts any string without constraints on format. If this is expected to be a comma-separated list, space-separated list, or follow another pattern, consider:

  • Adding Zod validation to enforce the expected format (e.g., .regex(/^(\w+,)*\w*$/) for comma-separated)
  • Documenting the expected format in a comment or the schema description

This will help prevent inconsistent data entry and improve maintainability.

app/routes/admin+/$organization.settings/forms/organization-settings.action.server.ts (1)

27-42: Consider transaction safety for dual updates.

The code performs two separate updates (organization and organizationSettings) without transaction wrapping. If the first succeeds and the second fails, the data could be in an inconsistent state.

Consider:

  • Wrapping both updates in a transaction if Kysely supports it
  • Or, ordering them so the less critical update (name) happens second
  • Providing more specific error messages that indicate which operation failed

Check if Kysely transactions are available:

#!/bin/bash
# Search for transaction usage patterns in the codebase
rg -nP 'transaction|\.trx\(|withTransaction' --type=ts -g 'app/services/*.ts' -g 'app/routes/**/*.server.ts'
app/routes/api.admin.recalculate.$organization.ts (1)

70-73: Sequential upserts may be slow for large PR datasets.

Each PR is upserted one at a time, which can be slow with many PRs. Consider batching or parallel execution with concurrency limits.

Example using Promise.all with chunking:

// Batch upsert in chunks
const BATCH_SIZE = 50
for (let i = 0; i < pulls.length; i += BATCH_SIZE) {
  const batch = pulls.slice(i, i + BATCH_SIZE)
  await Promise.all(batch.map((pr) => upsertPullRequest(pr)))
}
batch/provider/github/provider.ts (1)

154-155: Consider using push instead of spread for array accumulation.

Using spread operator in a loop ([...allPulls, ...pulls]) creates a new array on each iteration, resulting in O(n²) time complexity for large datasets.

Apply this diff for better performance:

-      allPulls = [...allPulls, ...pulls]
-      allReviewResponses = [...allReviewResponses, ...reviewResponses]
+      allPulls.push(...pulls)
+      allReviewResponses.push(...reviewResponses)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ca0ac9 and aee6c28.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .claude/hooks/format-on-edit.ts (1 hunks)
  • CLAUDE.md (5 hunks)
  • app/routes/admin+/$organization.settings/forms/index.ts (1 hunks)
  • app/routes/admin+/$organization.settings/forms/organization-settings.action.server.ts (2 hunks)
  • app/routes/admin+/$organization.settings/forms/organization-settings.tsx (2 hunks)
  • app/routes/admin+/$organization.settings/forms/recalculate.tsx (1 hunks)
  • app/routes/admin+/$organization.settings/functions/mutations.server.ts (1 hunks)
  • app/routes/admin+/$organization.settings/route.tsx (2 hunks)
  • app/routes/admin+/$organization.settings/types.ts (1 hunks)
  • app/routes/api.admin.recalculate.$organization.ts (1 hunks)
  • app/services/type.ts (1 hunks)
  • batch/db/queries.ts (2 hunks)
  • batch/provider/github/model.ts (1 hunks)
  • batch/provider/github/provider.ts (3 hunks)
  • batch/provider/github/pullrequest.ts (2 hunks)
  • package.json (2 hunks)
  • prisma/migrations/20251216104437_add_excluded_users_to_organization_settings/migration.sql (1 hunks)
  • prisma/migrations/migration_lock.toml (1 hunks)
  • prisma/schema.prisma (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
batch/provider/github/pullrequest.ts (2)
batch/provider/github/model.ts (1)
  • ShapedGitHubPullRequest (15-33)
batch/provider/github/review-response.ts (1)
  • analyzeReviewResponse (5-30)
app/routes/api.admin.recalculate.$organization.ts (4)
app/routes/admin+/$organization.settings/route.tsx (1)
  • loader (27-43)
batch/provider/index.ts (1)
  • createProvider (5-8)
batch/db/mutations.ts (1)
  • upsertPullRequest (4-46)
batch/bizlogic/export-spreadsheet.ts (2)
  • exportPullsToSpreadsheet (13-79)
  • exportReviewResponsesToSpreadsheet (81-117)
app/routes/admin+/$organization.settings/functions/mutations.server.ts (2)
app/services/type.ts (3)
  • DB (150-164)
  • Organization (65-72)
  • OrganizationSetting (73-82)
app/services/db.server.ts (3)
  • Updateable (18-18)
  • db (23-27)
  • sql (17-17)
batch/provider/github/provider.ts (2)
app/services/db.server.ts (1)
  • Selectable (18-18)
app/services/type.ts (2)
  • DB (150-164)
  • Repository (105-116)
app/routes/admin+/$organization.settings/forms/recalculate.tsx (5)
app/components/ui/card.tsx (6)
  • Card (68-68)
  • CardHeader (68-68)
  • CardTitle (68-68)
  • CardDescription (68-68)
  • CardContent (68-68)
  • CardFooter (68-68)
app/components/ui/stack.tsx (1)
  • Stack (38-46)
app/components/ui/progress.tsx (1)
  • Progress (29-29)
app/components/ui/alert.tsx (2)
  • Alert (66-66)
  • AlertDescription (66-66)
app/components/ui/button.tsx (1)
  • Button (58-58)
app/routes/admin+/$organization.settings/route.tsx (1)
app/routes/admin+/$organization.settings/forms/recalculate.tsx (1)
  • Recalculate (25-127)
app/routes/admin+/$organization.settings/forms/organization-settings.tsx (2)
app/components/ui/label.tsx (1)
  • Label (22-22)
app/components/ui/input.tsx (1)
  • Input (21-21)
app/routes/admin+/$organization.settings/forms/organization-settings.action.server.ts (1)
app/routes/admin+/$organization.settings/functions/mutations.server.ts (2)
  • updateOrganization (10-19)
  • updateOrganizationSetting (21-36)
🔇 Additional comments (19)
prisma/schema.prisma (1)

46-46: LGTM!

The excludedUsers field is properly defined with an appropriate default value and database mapping. The inline comment clearly documents the comma-separated format.

batch/db/queries.ts (1)

31-31: LGTM!

The excludedUsers field has been correctly added to both organization queries, following the existing patterns for selecting organization settings.

Also applies to: 103-103

app/services/type.ts (1)

79-79: LGTM!

The excludedUsers field has been correctly generated by prisma-kysely to match the Prisma schema definition.

app/routes/admin+/$organization.settings/forms/organization-settings.tsx (1)

52-52: LGTM!

The excluded users field has been properly integrated into the form with clear labeling, helpful placeholder text, and proper error handling. The implementation follows the existing form patterns consistently.

Also applies to: 122-137

package.json (1)

136-136: tsx in devDependencies is correct for Prisma seed scripts.

Seeding is primarily helpful when working in a development environment. The tsx package is appropriately placed in devDependencies because seeding is only triggered explicitly by running npx prisma db seed—not automatically in production. When the --production flag is set or NODE_ENV is set to production, npm will not install modules listed in devDependencies. This placement follows the standard pattern shown in Prisma's official Next.js documentation.

app/routes/admin+/$organization.settings/route.tsx (1)

11-11: LGTM! Clean integration of the Recalculate component.

The import and rendering of the Recalculate component follows the existing pattern and is appropriately positioned between the ExportSettings and DeleteOrganization sections.

Also applies to: 85-85

app/routes/admin+/$organization.settings/forms/index.ts (1)

5-5: LGTM! Consistent barrel export pattern.

The re-export of the recalculate module follows the existing pattern in the file.

CLAUDE.md (1)

52-58: LGTM! Helpful documentation improvements.

The added Tech Stack details and improved formatting enhance the documentation's clarity and usefulness for AI-assisted development.

prisma/migrations/20251216104437_add_excluded_users_to_organization_settings/migration.sql (1)

1-20: LGTM! Correct SQLite migration pattern.

The migration correctly uses SQLite's RedefineTables pattern to add the excluded_users column with appropriate defaults, foreign key constraints, and data preservation.

app/routes/admin+/$organization.settings/functions/mutations.server.ts (1)

21-36: Consider validating that the organizationSettings row exists before attempting to update it.

Kysely's UpdateResult returns numUpdatedRows indicating how many rows were updated, but the function doesn't validate this. If an organization lacks a corresponding organizationSettings row, the update executes successfully but affects zero rows. The caller awaits this function without checking the return value, so the failure would be silent.

Either ensure organizationSettings rows are created when organizations are created, or add a check in this function to verify at least one row was updated and throw if not.

batch/provider/github/pullrequest.ts (4)

21-22: LGTM! Sensible default exclusion list.

Good choice to exclude Copilot by default since it's a known AI assistant that shouldn't count toward pickup time calculations.


56-68: LGTM! Comprehensive filtering logic.

The filter correctly excludes:

  1. Bot accounts (suffix [bot])
  2. PR author (self-reviews shouldn't count)
  3. Custom excluded users list

Using r.user ?? '' handles the null case appropriately for the includes check.


74-74: Good null handling for author.

The res.author ?? '' fallback aligns with the analyzeReviewResponse function which can return null authors based on the ShapedGitHubReviewComment type.


30-39: The null/undefined handling suggestion is not necessary.

The Prisma schema defines excludedUsers as a non-nullable field with a default value of "" (line 46 of prisma/schema.prisma). The generated type enforces excludedUsers: string, and the Zod validation schema also treats it as a required string with a default. Since the database guarantees a non-null string value, the call to .split(',') will not throw a TypeError, and the suggested nullish coalescing operator is redundant.

Likely an incorrect or invalid review comment.

app/routes/api.admin.recalculate.$organization.ts (2)

89-98: Error handling and cleanup look correct.

The finally block properly removes the organizationId from runningJobs and closes the controller, ensuring cleanup happens regardless of success or failure.


102-108: LGTM! Correct SSE response headers.

The headers are properly set for SSE: text/event-stream, no-cache, and keep-alive.

batch/provider/github/provider.ts (3)

108-118: LGTM! Well-typed progress callback.

The optional onProgress callback with typed progress object (repo, current, total) provides a clean interface for tracking analysis progress.


129-134: LGTM! Progress tracking implementation.

Progress is correctly tracked and reported before processing each repository, giving accurate real-time feedback to the SSE consumer.


150-150: Ensure excludedUsers is not null before passing to buildPullRequests.

If organizationSetting.excludedUsers can be null from the database, this will cause issues in buildPullRequests when calling .split(). Ensure consistency with the null handling suggested in pullrequest.ts.

Comment on lines +38 to +40
const eventSource = new EventSource(
`/api/admin/recalculate/${organization}`,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find app/routes -name "*recalculate*" -o -path "*api*admin*" | head -20

Repository: coji/upflow

Length of output: 167


🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*/api/*" | grep -i recalc

Repository: coji/upflow

Length of output: 37


🏁 Script executed:

rg "recalculate" --type ts --type tsx -l app/routes/api

Repository: coji/upflow

Length of output: 82


🏁 Script executed:

fd "recalculate" app/routes

Repository: coji/upflow

Length of output: 167


🏁 Script executed:

rg "EventSource" app/routes --type tsx -A 5 -B 5

Repository: coji/upflow

Length of output: 82


🏁 Script executed:

find app/routes -type f | xargs grep -l "recalculate\|EventSource" | grep -E "\.(ts|tsx)$"

Repository: coji/upflow

Length of output: 223


🏁 Script executed:

find app -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "/api/admin/recalculate" 2>/dev/null | head -5

Repository: coji/upflow

Length of output: 117


🏁 Script executed:

cat app/routes/api.admin.recalculate.$organization.ts

Repository: coji/upflow

Length of output: 123


🏁 Script executed:

cat "app/routes/api.admin.recalculate.\$organization.ts"

Repository: coji/upflow

Length of output: 3287


🏁 Script executed:

rg "getSession\|requireAdmin\|requireUser" app/routes/api --type ts -A 3 | head -50

Repository: coji/upflow

Length of output: 109


🏁 Script executed:

rg "getSession|requireAdmin|requireUser" app/routes --type ts -A 2 | head -80

Repository: coji/upflow

Length of output: 1853


🏁 Script executed:

cat app/libs/auth.server.ts | head -100

Repository: coji/upflow

Length of output: 2642


🏁 Script executed:

rg "export.*function|export const" app/libs/auth.server.ts | head -30

Repository: coji/upflow

Length of output: 263


🏁 Script executed:

rg "export const requireUser|export const requireSuperAdmin" -A 15 app/libs/auth.server.ts

Repository: coji/upflow

Length of output: 488


🏁 Script executed:

rg "export const loader.*LoaderArgs" app/routes/api.admin* -A 10 | head -60

Repository: coji/upflow

Length of output: 456


Add authentication and authorization checks to the SSE endpoint.

The /api/admin/recalculate/{organization} endpoint currently has no authentication or permission validation. Import requireUser from ~/app/libs/auth.server and verify the authenticated user has admin access to the specified organization before processing the recalculation request. Currently, any unauthenticated user can trigger expensive operations by accessing this endpoint with any organization ID.

coji and others added 2 commits December 24, 2025 19:54
- Add requireSuperAdmin auth check to recalculate API endpoint
- Simplify redundant ternary in Alert variant
- Add error logging to format hook catch block

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove UPFLOW_DATA_DIR from cli.ts (no longer needed)
- Add getDataDir() helper that defaults to ./data
- Consolidate path resolution logic in path-builder.ts
- Simplify tests to focus on default behavior

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

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
app/routes/api.admin.recalculate.$organization.ts (1)

22-40: Race condition in concurrency check still exists.

The concurrency check at line 22 and the runningJobs.add() at line 40 are not atomic. Between checking and adding inside the stream's start(), another request could pass the check.

Move runningJobs.add(organizationId) immediately after the check passes (before creating the stream) to make the guard atomic within this process.

🔎 Proposed fix
   // 同時実行チェック
   if (runningJobs.has(organizationId)) {
     return new Response(
       JSON.stringify({ error: 'Recalculation already in progress' }),
       {
         status: 409,
         headers: { 'Content-Type': 'application/json' },
       },
     )
   }
+  runningJobs.add(organizationId)

   // SSE ストリームを作成
   const stream = new ReadableStream({
     async start(controller) {
       const encoder = new TextEncoder()
       const send = (data: Record<string, unknown>) => {
         controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
       }

-      runningJobs.add(organizationId)

       try {
app/routes/admin+/$organization.settings/forms/recalculate.tsx (2)

31-79: Missing EventSource cleanup and JSON parse error handling.

Two previously flagged issues remain:

  1. Memory leak: The EventSource is not tracked in a ref and won't be closed if the component unmounts during the operation.
  2. Uncaught exception: JSON.parse(event.data) at line 43 can throw on malformed data, crashing the handler.
🔎 Proposed fix
 export const Recalculate = () => {
   const { organization } = useParams()
   const [status, setStatus] = useState<Status>('idle')
   const [progress, setProgress] = useState<ProgressData | null>(null)
   const [message, setMessage] = useState<string>('')
+  const eventSourceRef = useRef<EventSource | null>(null)
+
+  useEffect(() => {
+    return () => {
+      eventSourceRef.current?.close()
+    }
+  }, [])

   const handleRecalculate = useCallback(() => {
     if (!organization) return

+    // Close any existing connection
+    eventSourceRef.current?.close()
+
     setStatus('running')
     setProgress(null)
     setMessage('Starting...')

     const eventSource = new EventSource(
       `/api/admin/recalculate/${organization}`,
     )
+    eventSourceRef.current = eventSource

     eventSource.onmessage = (event) => {
-      const data = JSON.parse(event.data)
+      let data
+      try {
+        data = JSON.parse(event.data)
+      } catch {
+        setStatus('error')
+        setMessage('Invalid server response')
+        eventSource.close()
+        return
+      }

       switch (data.type) {
         // ... cases unchanged
       }
     }

     eventSource.onerror = () => {
       setStatus('error')
       setMessage('Connection error occurred')
       eventSource.close()
+      eventSourceRef.current = null
     }
   }, [organization])

81-83: Guard against division by zero.

If progress.total is 0, the calculation produces NaN, which may cause rendering issues.

🔎 Proposed fix
   const progressPercent = progress
-    ? Math.round((progress.current / progress.total) * 100)
+    ? progress.total > 0
+      ? Math.round((progress.current / progress.total) * 100)
+      : 0
     : 0
🧹 Nitpick comments (1)
batch/helper/path-builder.test.ts (1)

4-8: Add test coverage for environment variable cases.

The current test only validates the default behavior. Consider adding test cases for:

  • When UPFLOW_DATA_DIR is set to a valid path
  • When UPFLOW_DATA_DIR is set to the string 'undefined' (to verify Line 9 in path-builder.ts)
🔎 Suggested additional test cases
+import { beforeEach, afterEach } from 'vitest'
+
 describe('getDataDir', () => {
+  const originalEnv = process.env.UPFLOW_DATA_DIR
+
+  afterEach(() => {
+    if (originalEnv === undefined) {
+      delete process.env.UPFLOW_DATA_DIR
+    } else {
+      process.env.UPFLOW_DATA_DIR = originalEnv
+    }
+  })
+
   test('returns default ./data', () => {
+    delete process.env.UPFLOW_DATA_DIR
     expect(getDataDir()).toBe('./data')
   })
+
+  test('returns UPFLOW_DATA_DIR when set', () => {
+    process.env.UPFLOW_DATA_DIR = '/custom/data/path'
+    expect(getDataDir()).toBe('/custom/data/path')
+  })
+
+  test('returns default when UPFLOW_DATA_DIR is string "undefined"', () => {
+    process.env.UPFLOW_DATA_DIR = 'undefined'
+    expect(getDataDir()).toBe('./data')
+  })
 })
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aee6c28 and 6f0dbb6.

📒 Files selected for processing (7)
  • .claude/hooks/format-on-edit.ts
  • app/routes/admin+/$organization.settings/forms/recalculate.tsx
  • app/routes/api.admin.recalculate.$organization.ts
  • batch/cli.ts
  • batch/helper/path-builder.test.ts
  • batch/helper/path-builder.ts
  • batch/helper/path.ts
💤 Files with no reviewable changes (1)
  • batch/cli.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .claude/hooks/format-on-edit.ts
🧰 Additional context used
🧬 Code graph analysis (4)
batch/helper/path-builder.ts (1)
batch/helper/index.ts (1)
  • path (3-3)
batch/helper/path.ts (1)
batch/helper/path-builder.ts (1)
  • getDataDir (7-14)
app/routes/api.admin.recalculate.$organization.ts (4)
app/libs/auth.server.ts (1)
  • requireSuperAdmin (151-160)
batch/provider/index.ts (1)
  • createProvider (5-8)
batch/db/mutations.ts (1)
  • upsertPullRequest (4-46)
batch/bizlogic/export-spreadsheet.ts (2)
  • exportPullsToSpreadsheet (13-79)
  • exportReviewResponsesToSpreadsheet (81-117)
batch/helper/path-builder.test.ts (1)
batch/helper/path-builder.ts (2)
  • getDataDir (7-14)
  • createPathBuilder (20-53)
🔇 Additional comments (8)
app/routes/api.admin.recalculate.$organization.ts (4)

1-12: LGTM on imports and module setup.

The imports are appropriate, and requireSuperAdmin correctly addresses the previously flagged authentication concern. The in-memory runningJobs Set is acceptable for single-instance deployments.


42-70: LGTM on organization loading and analysis.

The use of invariant for runtime validation is appropriate. The progress callback correctly streams per-repository updates to the client.


72-92: LGTM on data persistence and export.

Sequential upsert is a safe choice for reliability. The conditional spreadsheet export and completion message are handled correctly.


93-113: LGTM on error handling and response.

The finally block ensures proper cleanup of runningJobs and stream closure regardless of success or failure. SSE headers are correctly configured.

app/routes/admin+/$organization.settings/forms/recalculate.tsx (1)

85-118: LGTM on component rendering.

The UI structure is clean with appropriate conditional rendering for progress and alerts. The button correctly disables during processing to prevent duplicate triggers.

batch/helper/path-builder.ts (1)

26-26: LGTM! Good refactoring to centralize data directory logic.

The update to use getDataDir() successfully centralizes the data directory resolution logic, making it easier to maintain.

batch/helper/path.ts (1)

3-6: LGTM! Successful consolidation of data directory logic.

The refactoring correctly replaces the inlined data directory logic with the centralized getDataDir() helper, improving maintainability.

batch/helper/path-builder.test.ts (1)

10-20: LGTM! Path-builder tests correctly validate the refactored behavior.

The test properly validates that paths are constructed using the new getDataDir() base. The expected path 'data/json/...' is correct since path.join normalizes away the leading './' from './data'.

Comment on lines +7 to +14
export const getDataDir = () => {
const upflowDataDir = process.env.UPFLOW_DATA_DIR
if (upflowDataDir && upflowDataDir !== 'undefined') {
return upflowDataDir
}
// デフォルト: プロジェクトルートの data/
return './data'
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for scripts that might start the batch process
echo "=== Searching for batch startup patterns ==="
rg -n "batch|tsx|node" --type sh --type js --type json | grep -i "batch\|start\|run" | head -20

echo -e "\n=== Checking for package.json scripts ==="
fd -e json -e yml -e yaml | xargs grep -l "batch" 2>/dev/null | head -5

echo -e "\n=== Searching for documentation about working directory ==="
fd -e md | xargs rg -n "working directory|current working|cwd|project root" 2>/dev/null | head -20

echo -e "\n=== Looking for UPFLOW_DATA_DIR usage in codebase ==="
rg -n "UPFLOW_DATA_DIR" --type ts --type js

echo -e "\n=== Checking how getDataDir is used ==="
rg -n "getDataDir" --type ts --type js -A2

Repository: coji/upflow

Length of output: 1908


🏁 Script executed:

#!/bin/bash
# Check if 'undefined' string checks are common in this codebase
echo "=== Checking for 'undefined' string checks ==="
rg -n "!== ['\"]undefined['\"]|== ['\"]undefined['\"]" --type ts --type js | head -10

echo -e "\n=== Examining the path-builder.ts file in full context ==="
cat -n batch/helper/path-builder.ts 2>/dev/null || echo "File not found at that path, searching..."
fd "path-builder" --type ts

Repository: coji/upflow

Length of output: 2449


🏁 Script executed:

#!/bin/bash
# Search for Docker and deployment configs
echo "=== Looking for Docker and deployment configs ==="
fd -e Dockerfile -e dockerfile -e docker-compose.yml -e .env -e .env.example | head -10

echo -e "\n=== Checking Docker files for working directory ==="
fd -e Dockerfile | xargs cat 2>/dev/null | grep -A5 -B5 "WORKDIR\|RUN.*node\|ENTRYPOINT"

echo -e "\n=== Checking README or docs ==="
fd "README|readme" -e md | xargs head -50 2>/dev/null | head -100

echo -e "\n=== Looking for env variable patterns and comments ==="
rg -n "process.env" batch/helper/path-builder.ts -B2 -A2

Repository: coji/upflow

Length of output: 878


Document the working directory assumption and the 'undefined' environment variable check.

The relative path './data' is intentional per the code comment (the test explicitly validates this behavior), but the dependency on process working directory should be documented. Additionally, the defensive check upflowDataDir !== 'undefined' (checking for the literal string "undefined") is not explained in comments—clarify why this pattern is needed, as it suggests handling for misconfigured environments that set the variable to the string 'undefined' rather than leaving it unset.

🤖 Prompt for AI Agents
In batch/helper/path-builder.ts around lines 7 to 14, add an inline comment
above the default return documenting that './data' is a relative path resolved
from the process working directory (i.e., process.cwd()) and that tests rely on
this behavior; also add a short comment explaining the defensive check
upflowDataDir !== 'undefined' (it handles environments that mistakenly set the
variable to the literal string "undefined" instead of leaving it unset) and
optionally suggest normalizing the env var (e.g., trim and treat empty or
'undefined' strings as unset) so future maintainers understand the intent and
can safely change behavior.

- Fix command injection vulnerability in format hook (use execFileSync)
- Add EventSource cleanup on component unmount
- Add JSON.parse error handling for SSE messages
- Fix division by zero in progress calculation
- Fix race condition in concurrent execution check

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

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
app/routes/admin+/$organization.settings/forms/recalculate.tsx (1)

54-56: Consider URL-encoding the organization parameter.

If organization could contain special characters (spaces, ampersands, etc.), the URL could break or be misinterpreted. While route parameters are typically URL-safe slugs, encoding defensively is a good practice.

🔎 Proposed fix
     const eventSource = new EventSource(
-      `/api/admin/recalculate/${organization}`,
+      `/api/admin/recalculate/${encodeURIComponent(organization)}`,
     )
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f0dbb6 and ad95296.

📒 Files selected for processing (3)
  • .claude/hooks/format-on-edit.ts
  • app/routes/admin+/$organization.settings/forms/recalculate.tsx
  • app/routes/api.admin.recalculate.$organization.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/routes/api.admin.recalculate.$organization.ts
  • .claude/hooks/format-on-edit.ts
🧰 Additional context used
🧬 Code graph analysis (1)
app/routes/admin+/$organization.settings/forms/recalculate.tsx (1)
app/components/ui/stack.tsx (1)
  • Stack (38-46)
🔇 Additional comments (5)
app/routes/admin+/$organization.settings/forms/recalculate.tsx (5)

1-23: LGTM! Imports and type definitions are well-structured.

The imports now include useRef and useEffect as required for proper EventSource lifecycle management. The Status type and ProgressData interface provide clear type safety for the component state.


32-40: LGTM! EventSource cleanup on unmount is properly implemented.

The cleanup effect correctly closes the EventSource connection and nulls the ref when the component unmounts, preventing memory leaks and orphaned connections.


67-76: LGTM! JSON parsing error handling is properly implemented.

The try-catch block around JSON.parse prevents crashes from malformed server responses, with appropriate error state updates, connection cleanup, and ref nulling.


117-120: LGTM! Division-by-zero protection is in place.

The progress percentage calculation safely guards against NaN by checking progress.total > 0 before performing the division.


122-155: LGTM! UI rendering logic is well-structured.

The component renders appropriate UI states:

  • Progress bar with repository count shown during processing
  • Alert with contextual variant (destructive for errors)
  • Button disabled while running to prevent duplicate submissions

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