Skip to content

feat: Task Board View with Kanban & Batch Execution (#331)#338

Merged
frankbria merged 4 commits into
mainfrom
feat/331-task-board-view
Feb 6, 2026
Merged

feat: Task Board View with Kanban & Batch Execution (#331)#338
frankbria merged 4 commits into
mainfrom
feat/331-task-board-view

Conversation

@frankbria

@frankbria frankbria commented Feb 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implements the Phase 3 Task Board page with a 6-column Kanban layout (Backlog → Ready → In Progress → Blocked → Failed → Done)
  • Adds search with debounced filtering, status pill toggles, batch selection mode with serial/parallel strategy picker, and a task detail modal with Execute/Mark Ready actions
  • Includes 46 tests across 4 suites (TaskCard, TaskBoardView, TaskDetailModal, TaskFilters), keyboard accessibility (WCAG 2.1 SC 2.1.1), and responsive grid layout

New Components (7)

Component Purpose
TaskBoardView Main orchestrator — SWR data fetching, filtering state, batch execution
TaskBoardContent 6-column responsive Kanban grid
TaskColumn Status column with count badge and "No tasks" empty state
TaskCard Interactive card with status badge, dependency indicator, keyboard a11y
TaskDetailModal Radix Dialog with task metadata, Execute/Mark Ready actions
TaskFilters Search input (300ms debounce) + status pill filter toggles
BatchActionsBar Selection mode toggle, strategy picker (serial/parallel), batch execute

Also Includes

  • /tasks page route with workspace hydration guard
  • Tasks nav link enabled in sidebar
  • tasksApi extensions: getAll, getOne, updateStatus, startExecution, executeBatch
  • Batch execution types: BatchRequest, BatchStrategy, TaskListResponse
  • 3 new Shadcn UI primitives: Checkbox, Input, Select (Nova template)

Closes #331

Test plan

  • Build passes (next build) — /tasks route registered
  • Lint clean on all feature files (0 errors)
  • 200 tests passing across 20 suites (46 new tests for task components)
  • Code review: keyboard a11y, navigation fix, modal test coverage all addressed
  • Manual: Navigate to /tasks, verify Kanban columns render with mock/real API
  • Manual: Test search filtering, status pill toggles, batch selection mode
  • Manual: Click task card → detail modal opens with correct metadata

Summary by CodeRabbit

  • New Features

    • Enabled Tasks navigation and launched a kanban Task Board with columns, selectable task cards, detail modal, debounced search/status filters, batch selection with configurable strategies, and batch execute/clear actions.
    • Added new UI form controls (Checkbox, Input, Select) and task-related icons.
  • Tests

    • Added extensive tests for Task Board, cards, detail modal, filters, batch actions, and sidebar behavior.
  • Chores

    • Added UI libraries to support new components.

Implements the Phase 3 Task Board page with full Kanban board, filtering,
batch execution controls, and task detail modal.

Components:
- TaskBoardView: Main orchestrator with SWR data fetching, filtering, and batch state
- TaskBoardContent: 6-column Kanban layout (Backlog→Done) with responsive grid
- TaskColumn: Status column with count badge and empty state
- TaskCard: Interactive card with status badge, dependency indicator, a11y keyboard support
- TaskDetailModal: Dialog with task metadata, Execute/Mark Ready actions
- TaskFilters: Search with 300ms debounce + status pill toggles
- BatchActionsBar: Selection mode toggle, strategy picker, batch execute

Also adds:
- /tasks page route with workspace hydration guard
- Tasks nav link enabled in AppSidebar
- tasksApi extensions (getAll, getOne, updateStatus, startExecution, executeBatch)
- Batch execution types (BatchRequest, BatchStrategy, TaskListResponse)
- 3 new Shadcn UI components (Checkbox, Input, Select)
- 46 tests across 4 test suites (TaskCard, TaskBoardView, TaskDetailModal, TaskFilters)
@coderabbitai

coderabbitai Bot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a Task Board feature with Kanban UI (board, columns, cards, filters, batch actions, detail modal), new task API client methods and types, UI primitives (Checkbox/Input/Select), tests, icon mock exports, and enables the Tasks sidebar/page with workspace hydration handling.

Changes

Cohort / File(s) Summary
Icon Mocks
web-ui/__mocks__/@hugeicons/react.js
Adds mocked icon exports: PlayCircleIcon, LinkCircleIcon, Search01Icon, CheckListIcon.
Navigation & Page
web-ui/src/components/layout/AppSidebar.tsx, web-ui/src/app/tasks/page.tsx, web-ui/__tests__/components/layout/AppSidebar.test.tsx
Enables Tasks nav item; adds client-side /tasks page with workspace hydration guards; updates sidebar test expectation for Tasks link.
Core Task UI
web-ui/src/components/tasks/...
web-ui/src/components/tasks/TaskBoardView.tsx, .../TaskBoardContent.tsx, .../TaskColumn.tsx, .../TaskCard.tsx, .../TaskDetailModal.tsx, .../index.ts
Introduces TaskBoardView and supporting components (content, columns, cards, detail modal) and a barrel export for task UI.
Filtering & Batch Controls
web-ui/src/components/tasks/TaskFilters.tsx, web-ui/src/components/tasks/BatchActionsBar.tsx
Adds debounced search/status filters and BatchActionsBar with selection mode, strategy selector, execute/clear actions.
UI Primitives
web-ui/src/components/ui/checkbox.tsx, web-ui/src/components/ui/input.tsx, web-ui/src/components/ui/select.tsx
Adds Checkbox, Input, and a styled Select built on Radix primitives (new components/wrappers).
API & Types
web-ui/src/lib/api.ts, web-ui/src/types/index.ts
Adds tasksApi methods (getOne, updateStatus, startExecution, executeBatch) and types (BatchStrategy, BatchExecutionRequest, StartExecutionResponse, TaskStartResponse); makes Task.created_at/updated_at optional; expands error normalization.
Package Dependencies
web-ui/package.json
Adds @radix-ui/react-checkbox and @radix-ui/react-select dependencies.
Tests
web-ui/__tests__/components/tasks/*, web-ui/__tests__/lib/api.test.ts
Adds comprehensive tests for TaskBoardView, TaskCard, TaskDetailModal, TaskFilters; extends API error normalization tests; updates AppSidebar test.
Server Response Model
codeframe/ui/routers/tasks_v2.py
Adds optional estimated_hours, created_at, updated_at to TaskResponse and populates them in list/get/update endpoints.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as TasksPage
    participant Board as TaskBoardView
    participant API as tasksApi
    participant Backend as /api/v2/tasks

    User->>Page: Navigate to /tasks
    Page->>Board: render with workspacePath
    Board->>API: SWR GET /api/v2/tasks (workspacePath)
    API->>Backend: GET /api/v2/tasks?workspace_path=...
    Backend-->>API: Task[]
    API-->>Board: data
    Board->>Board: group by status, render TaskBoardContent
    User->>Board: Filter / search / toggle selection / open modal / execute
    Board->>API: POST /api/v2/tasks/{id}/execute or POST /api/v2/batches/run or PATCH /api/v2/tasks/{id}
    API->>Backend: execute/update request
    Backend-->>API: execution/update response
    API-->>Board: result
    Board->>Board: mutate/revalidate SWR, update UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop-hop — the kanban wakes to play,

Cards in columns, tasks on display.
Search and select, batch-start the queue,
Little rabbit clicks — workflows anew.
Cheers to tiny hops and big things done!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature: Task Board View with Kanban layout and Batch Execution, matching the primary changes in the changeset.
Linked Issues check ✅ Passed All primary coding objectives from issue #331 are met: Kanban columns, task cards, detail modal, search/filter, batch execution with strategy selection, single-task actions, API integration, and /tasks route.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the Task Board View feature. No unrelated modifications or technical debt cleanups outside the issue objectives are present.

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

✨ 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 feat/331-task-board-view

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

@macroscopeapp

macroscopeapp Bot commented Feb 6, 2026

Copy link
Copy Markdown
Contributor

Add Kanban Task Board with batch execution on /tasks and include estimated_hours and ISO8601 timestamps in TaskResponse

Introduce a Kanban Task Board UI with single and batch execution, client-side filters (including a 300ms debounced search), and a Task Detail modal; expand backend TaskResponse to return estimated_hours, created_at, and updated_at as ISO8601, and add task execution and status update endpoints used by the UI.

📍Where to Start

Start with the Task Board flow in TaskBoardView in page.tsx, then review API integrations in api.ts and backend schema/handlers in tasks_v2.py.


Macroscope summarized e35112f.

@claude

claude Bot commented Feb 6, 2026

Copy link
Copy Markdown

Code Review: Task Board View Implementation

This is a well-structured Phase 3 feature implementation. Here's my detailed review:

✅ Strengths

  1. Excellent Test Coverage: 46 new tests with comprehensive scenarios
  2. Accessibility: WCAG 2.1 compliance with keyboard navigation, ARIA labels
  3. Clean Component Architecture: 7 focused components with single-responsibility
  4. State Management: Proper SWR usage with optimistic updates
  5. Type Safety: Full TypeScript coverage

🔍 Critical Issues

1. API Contract Mismatch - updateStatus (CRITICAL)

Location: web-ui/src/lib/api.ts:150-161

The updateStatus returns Promise<Task> but backend returns TaskResponse which lacks estimated_hours, created_at, updated_at. This could cause runtime errors.

Fix: Update return type or handle missing fields in components.

2. Missing Backend Endpoint (CRITICAL)

Location: web-ui/src/lib/api.ts:138-148

Cannot find GET /api/v2/tasks endpoint in tasks_v2.py. Found only:

  • GET /api/v2/tasks/{task_id}
  • PATCH /api/v2/tasks/{task_id}
  • POST /api/v2/tasks/{task_id}/start

Fix: Verify endpoint exists or update frontend.

⚠️ Minor Issues

3. Error State Persistence

Location: web-ui/src/components/tasks/TaskBoardView.tsx:211-215

The actionError persists until next action. Consider auto-dismissing after 5 seconds or adding dismiss button.

4. Race Condition Handling

Location: web-ui/src/components/tasks/TaskDetailModal.tsx:66-92

Good use of cancelled flag! Consider if workspacePath should be in deps array.

📊 Performance - All Good ✅

  • useMemo for filtering
  • SWR caching
  • useCallback for handlers

🔒 Security - All Good ✅

  • React XSS protection
  • Proper CORS with credentials
  • Backend input validation

🎯 Recommendations

Must Fix:

  1. Verify GET /api/v2/tasks endpoint
  2. Fix updateStatus return type mismatch

Should Fix:
3. Add error auto-dismiss
4. Verify batch execution endpoint

Nice to Have:
5. Extract status mappings to shared file
6. Add loading indicators for batch execution
7. Add integration tests

✨ Overall

High-quality implementation with excellent testing and accessibility. Main concern is API contract alignment.

Recommendation: Approve with minor changes - fix API issues and it's production-ready.

Great work! 🎉

@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

🤖 Fix all issues with AI agents
In `@web-ui/src/components/tasks/TaskBoardView.tsx`:
- Around line 127-145: handleExecuteBatch currently starts the batch via
tasksApi.executeBatch and resets selection but never navigates to the Execution
Monitor; change it to capture the executeBatch response (const resp = await
tasksApi.executeBatch(...)), extract resp.batch_id (or resp.batchId), call
router.push(`/executions/${batchId}`) after successful start (you can navigate
immediately or after await mutate()), and only then clear selection and
setSelectionMode(false); update references in handleExecuteBatch to use the
returned batch_id and ensure a router (useRouter or history) is available in the
component before calling router.push.

In `@web-ui/src/components/tasks/TaskDetailModal.tsx`:
- Around line 94-107: The modal-level error can persist after a failed
handleMarkReady call; clear the error at the start of handleMarkReady by calling
setError('') (or null) before initiating the API call so any previous error does
not remain after a subsequent success; update the handleMarkReady function
(which calls tasksApi.updateStatus, setIsUpdating, setTask, and onStatusChange)
to reset error state at the beginning and preserve the existing
try/catch/finally behavior.
🧹 Nitpick comments (9)
web-ui/__tests__/components/tasks/TaskBoardView.test.tsx (1)

156-166: Fragile filter-pill selector relies on Badge internal markup.

btn.querySelector('div') distinguishes filter pills from other buttons by assuming Badge renders a <div>. If Badge's markup changes, readyFilter will be undefined and the non-null assertion on line 161 will throw a misleading error instead of a clear test failure.

Consider using a data-testid on the filter pill buttons in TaskFilters.tsx, or at minimum add a descriptive assertion before the !:

Suggested improvement
     const readyFilter = filterButtons.find(
       (btn) => btn.textContent === 'Ready' && btn.querySelector('div')
     );
-    await user.click(readyFilter!);
+    expect(readyFilter).toBeDefined();
+    await user.click(readyFilter!);

A more robust approach would be adding data-testid="status-filter-READY" to the filter pill buttons in TaskFilters.tsx and querying by that.

web-ui/src/components/tasks/TaskBoardContent.tsx (1)

37-48: Tasks with statuses outside COLUMN_ORDER are silently dropped.

The if (grouped[task.status]) guard on line 43 means any task whose status isn't in the 6-item COLUMN_ORDER (e.g., MERGED or a future status) will be silently excluded from the board. This is likely intentional for the Kanban view, but worth a brief inline comment so future maintainers don't wonder why tasks go missing.

Optional: Add clarifying comment
     for (const task of tasks) {
+      // Only include tasks whose status matches a visible Kanban column;
+      // other statuses (e.g. MERGED) are intentionally excluded.
       if (grouped[task.status]) {
         grouped[task.status].push(task);
       }
     }
web-ui/src/components/tasks/BatchActionsBar.tsx (1)

83-90: Minor: native <button> for "Clear" vs <Button> used elsewhere.

The "Clear" control uses a raw <button> while the rest of the bar uses the <Button> component. This is likely intentional for the link-style appearance, but worth noting it lacks the type="button" attribute, which means it defaults to type="submit" inside a <form>.

Add explicit type to prevent accidental form submission if ever wrapped in a form
          {selectedCount > 0 && (
            <button
+             type="button"
              onClick={onClearSelection}
              className="text-xs text-muted-foreground hover:text-foreground"
            >
              Clear
            </button>
          )}
web-ui/src/components/tasks/TaskCard.tsx (2)

10-30: Duplicated status mappings — extract to a shared module.

STATUS_BADGE_VARIANT and STATUS_LABEL are identically defined in both TaskCard.tsx and TaskDetailModal.tsx (lines 24–42). Consider extracting them to a shared file (e.g., task-utils.ts or a constants file) to avoid maintaining two copies.


77-79: as never cast on Badge variant is fragile.

This bypasses type-checking entirely. If the Badge variant prop is a union, you could type STATUS_BADGE_VARIANT with the actual variant union (e.g., import the BadgeProps["variant"] type from the badge component) to get compile-time safety.

web-ui/src/types/index.ts (1)

86-100: Consider using BatchStrategy / TaskStatus instead of string for response fields.

StartExecutionResponse.strategy (Line 90) and TaskStartResponse.status (Line 98) are typed as string, while the corresponding request types use the stricter union types. If the backend is guaranteed to return one of the known values, narrowing these would provide better type safety downstream.

Proposed tighter typing
 export interface StartExecutionResponse {
   success: boolean;
   batch_id: string;
   task_count: number;
-  strategy: string;
+  strategy: BatchStrategy;
   message: string;
 }

 export interface TaskStartResponse {
   success: boolean;
   run_id: string;
   task_id: string;
-  status: string;
+  status: TaskStatus;
   message: string;
 }
web-ui/src/components/tasks/TaskFilters.tsx (2)

36-46: Sync effect can trigger an unnecessary debounce cycle.

When searchQuery changes from the parent (Line 44–46), localQuery updates, which fires the debounce effect (Line 36–41), calling onSearchChange again with the same value after 300ms. This is a no-op but still schedules an unnecessary timer.

Not a bug — the value is idempotent — but if it becomes a concern, you could guard the debounce to skip when localQuery matches the incoming prop.


66-69: Add type="button" to pill buttons.

Same as the Clear button in BatchActionsBar — these <button> elements default to type="submit" and could cause unintended form submissions if the component is ever placed inside a <form>.

Proposed fix
            <button
              key={value}
+             type="button"
              onClick={() => onStatusFilter(isActive ? null : value)}

Also apply to the Clear button on Line 85:

          <button
+           type="button"
            onClick={() => onStatusFilter(null)}
web-ui/src/components/tasks/TaskBoardView.tsx (1)

46-64: Client-side filtering works but doesn't scale.

All tasks are fetched in a single call and filtered in useMemo. This is fine for moderate task counts, but if workspaces grow to thousands of tasks, consider passing the status filter to tasksApi.getAll (which already accepts an optional status parameter) to offload filtering to the server.

Not a problem today — just an architectural note for future scale.

Comment on lines +127 to +145
const handleExecuteBatch = useCallback(async () => {
if (selectedTaskIds.size === 0) return;
setIsExecuting(true);
setActionError(null);
try {
await tasksApi.executeBatch(workspacePath, {
task_ids: Array.from(selectedTaskIds),
strategy: batchStrategy,
});
setSelectionMode(false);
setSelectedTaskIds(new Set());
await mutate();
} catch (err) {
const apiErr = err as ApiError;
setActionError(apiErr.detail || 'Failed to start batch execution');
} finally {
setIsExecuting(false);
}
}, [workspacePath, selectedTaskIds, batchStrategy, mutate]);

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.

⚠️ Potential issue | 🟡 Minor

Missing navigation to Execution Monitor after batch execution.

The PR objectives state: "Execute Batch action that starts a batch run and navigates to the Execution Monitor." Currently, handleExecuteBatch resets selection and revalidates but doesn't navigate. The executeBatch response includes a batch_id that could be used for navigation (e.g., router.push(\/executions/${batchId}`)`).

Is this intentionally deferred, or should it be added now?

🤖 Prompt for AI Agents
In `@web-ui/src/components/tasks/TaskBoardView.tsx` around lines 127 - 145,
handleExecuteBatch currently starts the batch via tasksApi.executeBatch and
resets selection but never navigates to the Execution Monitor; change it to
capture the executeBatch response (const resp = await
tasksApi.executeBatch(...)), extract resp.batch_id (or resp.batchId), call
router.push(`/executions/${batchId}`) after successful start (you can navigate
immediately or after await mutate()), and only then clear selection and
setSelectionMode(false); update references in handleExecuteBatch to use the
returned batch_id and ensure a router (useRouter or history) is available in the
component before calling router.push.

Comment on lines +94 to +107
const handleMarkReady = async () => {
if (!task) return;
setIsUpdating(true);
try {
const updated = await tasksApi.updateStatus(workspacePath, task.id, 'READY');
setTask(updated);
onStatusChange();
} catch (err) {
const apiErr = err as ApiError;
setError(apiErr.detail || 'Failed to update status');
} finally {
setIsUpdating(false);
}
};

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.

⚠️ Potential issue | 🟡 Minor

Error from handleMarkReady may persist as stale state across interactions.

If handleMarkReady fails (Line 103), the error is set but only cleared on the next open/close cycle (Line 69). A successful subsequent action (like executing) won't clear this modal-level error. Consider clearing error at the start of handleMarkReady:

Proposed fix
   const handleMarkReady = async () => {
     if (!task) return;
     setIsUpdating(true);
+    setError(null);
     try {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleMarkReady = async () => {
if (!task) return;
setIsUpdating(true);
try {
const updated = await tasksApi.updateStatus(workspacePath, task.id, 'READY');
setTask(updated);
onStatusChange();
} catch (err) {
const apiErr = err as ApiError;
setError(apiErr.detail || 'Failed to update status');
} finally {
setIsUpdating(false);
}
};
const handleMarkReady = async () => {
if (!task) return;
setIsUpdating(true);
setError(null);
try {
const updated = await tasksApi.updateStatus(workspacePath, task.id, 'READY');
setTask(updated);
onStatusChange();
} catch (err) {
const apiErr = err as ApiError;
setError(apiErr.detail || 'Failed to update status');
} finally {
setIsUpdating(false);
}
};
🤖 Prompt for AI Agents
In `@web-ui/src/components/tasks/TaskDetailModal.tsx` around lines 94 - 107, The
modal-level error can persist after a failed handleMarkReady call; clear the
error at the start of handleMarkReady by calling setError('') (or null) before
initiating the API call so any previous error does not remain after a subsequent
success; update the handleMarkReady function (which calls tasksApi.updateStatus,
setIsUpdating, setTask, and onStatusChange) to reset error state at the
beginning and preserve the existing try/catch/finally behavior.

import type { Task, TaskStatus } from '@/types';

/** Column display order matches the task lifecycle. */
const COLUMN_ORDER: TaskStatus[] = [

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.

🟡 Medium

tasks/TaskBoardContent.tsx:8 MERGED is a valid TaskStatus but is missing from COLUMN_ORDER, so merged tasks are silently dropped. Consider adding 'MERGED' to the array.

🚀 Want me to fix this? Reply ex: "fix it for me".

Comment on lines +55 to +60
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick(task.id);
}
}}

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.

🟡 Medium

tasks/TaskCard.tsx:55 The onKeyDown handler intercepts Enter/Space from nested buttons and checkbox, breaking their keyboard accessibility. Consider checking e.target === e.currentTarget before handling.

Suggested change
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick(task.id);
}
}}
onKeyDown={(e) => {
if (e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
onClick(task.id);
}
}}

🚀 Want me to fix this? Reply ex: "fix it for me".

Backend TaskResponse was missing estimated_hours, created_at, and
updated_at fields that the core Task model has and the frontend expects.
Added these fields to TaskResponse and all 3 serialization points.
Also made created_at/updated_at optional in the frontend Task type
for resilience.
@claude

claude Bot commented Feb 6, 2026

Copy link
Copy Markdown

PR Review: Phase 3 Task Board Implementation

This is a comprehensive and well-structured implementation of the Task Board feature. The code quality is excellent overall, with good separation of concerns, accessibility features, and extensive test coverage. Here are my findings:

✅ Strengths

  1. Excellent Component Architecture

    • Clean separation between presentation (TaskCard, TaskColumn) and orchestration (TaskBoardView)
    • Proper state management with React hooks (useState, useMemo, useCallback)
    • Good use of SWR for data fetching with proper error/loading states
  2. Strong Accessibility (a11y)

    • Keyboard navigation on TaskCard (web-ui/src/components/tasks/TaskCard.tsx:55-60)
    • Proper ARIA labels and roles
    • Focus management with focus-visible styles
  3. Comprehensive Test Coverage

    • 46 tests across 4 test suites
    • Good use of fake timers to handle debounced search
    • Proper mocking of API calls and SWR
  4. Performance Optimizations

    • Debounced search (300ms) in TaskFilters.tsx:36-40
    • useMemo for filtered tasks (TaskBoardView.tsx:46-64)
    • useCallback for handlers to prevent unnecessary re-renders

🔍 Issues Found

1. Memory Leak Risk in TaskFilters (Medium Priority)

File: web-ui/src/components/tasks/TaskFilters.tsx:36-40

The timeout cleanup in the useEffect could potentially be called with an undefined timer:

return () => clearTimeout(timerRef.current); // timerRef.current might be undefined

Fix: Ensure timer is defined before clearing:

return () => {
  if (timerRef.current) {
    clearTimeout(timerRef.current);
  }
};

2. Type Assertion Could Hide Type Errors (Low Priority)

File: web-ui/src/components/tasks/TaskCard.tsx:78

variant={STATUS_BADGE_VARIANT[task.status] as never}

Using as never bypasses TypeScript's type checking. This could hide mismatches between backend TaskStatus and badge variants.

Recommendation: Define proper Badge variant types or add runtime validation.

3. API Error Handling Could Be More Specific (Low Priority)

File: web-ui/src/components/tasks/TaskBoardView.tsx:98-111

Error handling casts all errors to ApiError without checking:

const apiErr = err as ApiError;

Recommendation: Add type guard or instanceof check before casting.

4. Potential Race Condition in TaskDetailModal (Low Priority)

File: web-ui/src/components/tasks/TaskDetailModal.tsx:66-92

The cancelled flag pattern is good, but there's a potential race if the modal closes while an API call is in flight. The cleanup function sets cancelled = true but doesn't abort the fetch.

Recommendation: Consider using AbortController for proper request cancellation.

🛡️ Security Considerations

  1. Input Sanitization

    • Search input is properly handled through React's controlled components
    • No direct DOM manipulation or dangerouslySetInnerHTML
  2. XSS Protection

    • Task descriptions rendered safely through React (TaskCard.tsx:96-98)
    • No innerHTML usage
  3. CSRF Protection ℹ️

    • API client uses withCredentials: true (api.ts:61)
    • Ensure backend has proper CORS and CSRF token validation

📊 Performance Considerations

  1. Large Task Lists

    • Current implementation loads all tasks into memory
    • Consider virtualization (react-window) if dealing with >100 tasks
    • Filter/search operates on full dataset client-side
  2. Batch Operations

    • Good: Disabled state prevents double-submission (BatchActionsBar.tsx:71)
    • Consider: Adding optimistic updates for better perceived performance

🧪 Test Coverage Assessment

Excellent coverage with proper testing of:

  • Loading/error states
  • User interactions (clicking, keyboard navigation)
  • Filtering and search
  • Batch selection mode
  • Modal interactions

Missing coverage:

  • Integration tests for the full /tasks page
  • Error recovery scenarios (network retry)
  • Edge cases (empty task lists, very long titles)

📝 Code Style & Best Practices

  1. Consistent with CLAUDE.md guidelines

    • Follows v2 architecture (thin UI adapter over core)
    • No backend logic in frontend
    • Proper TypeScript usage
  2. Good Code Organization

    • Clear component boundaries
    • Logical file structure in /components/tasks/
    • Proper barrel exports (index.ts)

🚀 Recommendations

  1. Address the memory leak in TaskFilters (do this before merge)
  2. Consider adding loading skeletons during batch execution for better UX
  3. Add error boundary around TaskBoardView for graceful degradation
  4. Document the batch execution strategies (serial/parallel/auto) in user-facing tooltips
  5. Consider adding task count badges on status filter pills

📋 Summary

This PR is ready to merge after addressing the TaskFilters cleanup issue. The implementation is solid, well-tested, and follows the project's architectural patterns. The team has done excellent work on accessibility and code organization.

Approval Status: Approved pending minor fix ✅

Radix Dialog requires DialogTitle to always be present for screen
reader accessibility. Added a hidden DialogTitle for loading and
error states where the task data isn't available yet.
@claude

claude Bot commented Feb 6, 2026

Copy link
Copy Markdown

Follow-Up Review: All Critical Issues Addressed ✅

Thank you for the prompt fixes! I've reviewed the three commits since my initial review, and I'm pleased to confirm that all the issues I raised have been properly addressed.

✅ Issues Resolved

1. API Contract Mismatch - FIXED (commit 54bdd84)

Original Issue: TaskResponse lacked estimated_hours, created_at, updated_at fields

Resolution: ✅ Properly added to TaskResponse model in codeframe/ui/routers/tasks_v2.py:119-121:

estimated_hours: Optional[float] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None

All three API endpoints (list_tasks, get_task, update_task) now properly serialize these fields with safe None handling and isoformat() conversion.

2. Missing Backend Endpoint - CONFIRMED EXISTS

Original Issue: I couldn't find GET /api/v2/tasks endpoint

Resolution: ✅ Endpoint exists at codeframe/ui/routers/tasks_v2.py:146 with proper decorator:

@router.get("", response_model=TaskListResponse)

This was my error - the endpoint was always there. Apologies for the confusion!

3. Accessibility Enhancement - EXCELLENT FIX (commit 0985b35)

New Issue Found: Radix Dialog requires DialogTitle for screen reader accessibility in all states

Resolution: ✅ Added sr-only DialogTitle/DialogDescription for loading/error states in TaskDetailModal.tsx:112-117. This is a textbook accessibility fix - proper WCAG 2.1 compliance!

🔍 Outstanding Items from Previous Review

Minor Issues (non-blocking)

  1. TaskFilters cleanup - Still present but acceptable:

    return () => clearTimeout(timerRef.current); // timerRef.current could be undefined

    Status: The risk is minimal since clearTimeout(undefined) is safe per spec, but for defensive programming, consider:

    return () => { if (timerRef.current) clearTimeout(timerRef.current); };
  2. Type assertion in TaskCard.tsx:78 - Still uses as never:

    variant={STATUS_BADGE_VARIANT[task.status] as never}

    Status: Non-blocking. Consider defining Badge variant union type in a future refactor.

  3. Error auto-dismiss in TaskBoardView - Errors persist until next action:
    Status: UX enhancement for future iteration. Not critical.

🎯 Final Assessment

Status: APPROVED ✅

All critical issues have been resolved. The remaining items are minor code quality suggestions that can be addressed in future iterations. The implementation is:

  • ✅ Functionally complete
  • ✅ Type-safe with proper API contracts
  • ✅ Accessible (WCAG 2.1 compliant)
  • ✅ Well-tested (46 tests)
  • ✅ Following CLAUDE.md architecture (thin UI adapter over core)

Recommendation: Merge immediately. This is production-ready code.

Excellent work on the quick turnaround! The attention to accessibility (DialogTitle fix) shows strong attention to detail. 🚀

Backend api_error() returns {error, code, detail} objects as HTTPException
detail. The axios interceptor's normalizeErrorDetail only handled strings
and validation arrays, passing objects through unmodified. This caused
"Objects are not valid as React child" when the error object was rendered.

Now extracts the human-readable .error or .detail string from structured
error objects.

@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

🤖 Fix all issues with AI agents
In `@web-ui/src/components/tasks/TaskDetailModal.tsx`:
- Around line 66-71: In TaskDetailModal's useEffect, the early-return branch
currently clears task and error but not isLoading, which can leave a stale
spinner; update that branch to also call setIsLoading(false) so isLoading is
reset whenever the modal closes or taskId is falsy, and ensure the fetch-start
path still sets setIsLoading(true) and the fetch finally path clears it as
before (referencing useEffect, setTask, setError, setIsLoading, and the
cancelled flag).
🧹 Nitpick comments (1)
web-ui/src/components/tasks/TaskDetailModal.tsx (1)

136-136: as never cast bypasses Badge variant type-safety.

If Badge exposes its variant union (e.g., via VariantProps), you can type the map values directly and drop the cast. This keeps the compiler checking that every mapped value is actually a valid variant.

Example
-const STATUS_BADGE_VARIANT: Record<TaskStatus, string> = {
+import type { VariantProps } from 'class-variance-authority';
+// Assuming badgeVariants is exported from Badge
+type BadgeVariant = NonNullable<VariantProps<typeof badgeVariants>['variant']>;
+
+const STATUS_BADGE_VARIANT: Record<TaskStatus, BadgeVariant> = {
   BACKLOG: 'backlog',
   ...
 };

Then at the call site:

-<Badge variant={STATUS_BADGE_VARIANT[task.status] as never}>
+<Badge variant={STATUS_BADGE_VARIANT[task.status]}>

Comment on lines +66 to +71
useEffect(() => {
if (!open || !taskId) {
setTask(null);
setError(null);
return;
}

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.

⚠️ Potential issue | 🟠 Major

Stale isLoading can cause a permanent spinner.

The early-return branch resets task and error but not isLoading. If the dialog closes while a fetch is in-flight, the cancelled flag prevents the .finally() from clearing isLoading. On the next open with taskId: null (or a rapid close/reopen cycle), isLoading remains true with no pending fetch, rendering an infinite spinner.

Proposed fix
   useEffect(() => {
     if (!open || !taskId) {
       setTask(null);
       setError(null);
+      setIsLoading(false);
       return;
     }
🤖 Prompt for AI Agents
In `@web-ui/src/components/tasks/TaskDetailModal.tsx` around lines 66 - 71, In
TaskDetailModal's useEffect, the early-return branch currently clears task and
error but not isLoading, which can leave a stale spinner; update that branch to
also call setIsLoading(false) so isLoading is reset whenever the modal closes or
taskId is falsy, and ensure the fetch-start path still sets setIsLoading(true)
and the fetch finally path clears it as before (referencing useEffect, setTask,
setError, setIsLoading, and the cancelled flag).

@claude

claude Bot commented Feb 6, 2026

Copy link
Copy Markdown

Code Review: Task Board View Implementation

This is an excellent Phase 3 implementation that delivers a fully-featured Kanban board UI. The code quality is high, architecture is sound, and test coverage is comprehensive. Here's my detailed feedback:

✅ Strengths

1. Architecture & Design Patterns

  • Proper separation of concerns: TaskBoardView orchestrates, child components handle specific UI concerns
  • Headless data fetching: SWR with proper error handling and optimistic updates
  • Immutable state management: Uses Sets correctly with proper immutability (lines 75-82 in TaskBoardView)
  • Smart memoization: filteredTasks useMemo prevents unnecessary re-renders (lines 46-64)

2. Accessibility (WCAG 2.1 Compliance) ✨

  • Keyboard navigation: TaskCard implements proper onKeyDown with Enter/Space (lines 55-60)
  • ARIA labels: All interactive elements have proper labels (line 63, 74)
  • DialogTitle fix: Excellent fix for Radix Dialog accessibility requirement with sr-only title for loading/error states (lines 113-117)
  • Focus management: Proper focus-visible states throughout

3. API Integration

  • Error normalization: normalizeErrorDetail() elegantly handles FastAPI's three error formats (string, validation array, structured object) - this is a crucial fix
  • Backend alignment: TaskResponse now includes estimated_hours, created_at, updated_at fields (lines 119-124 in tasks_v2.py)
  • Type safety: Strong TypeScript contracts matching backend models

4. Test Coverage (46 new tests)

  • Comprehensive unit tests for all components
  • Edge case coverage (empty states, errors, loading)
  • User interaction tests with @testing-library/user-event
  • Fake timers properly handled for debounced search

5. User Experience

  • 300ms debounced search prevents excessive API calls (TaskFilters lines 36-41)
  • Loading skeletons with proper shimmer effect
  • Optimistic UI updates with SWR mutate after actions
  • Clear error states with actionable messages

🔍 Issues & Recommendations

🔴 Critical Issues

1. Race Condition in TaskDetailModal

The cleanup function in the useEffect sets cancelled = true but doesn't clear the loading state. If the modal closes before the API call completes, it stays in loading state when reopened.

Location: web-ui/src/components/tasks/TaskDetailModal.tsx:89-91

Current:

return () => {
  cancelled = true;
};

Fix:

return () => {
  cancelled = true;
  setIsLoading(false);
};

🟡 Medium Priority

2. Selection State Lost on Batch Error

In handleExecuteBatch, selection is cleared even when batch execution fails. Users lose their selection and must re-select.

Location: TaskBoardView.tsx:136-137

Issue: Lines execute regardless of try/catch outcome.

Fix: Move setSelectionMode(false) and setSelectedTaskIds(new Set()) inside the try block, before the catch.

3. Debounce Ineffective Due to Unstable Dependency

onSearchChange in dependency array (TaskFilters.tsx:41) isn't memoized in parent, causing debounce to reset on every parent re-render.

Fix Options:

  1. Wrap setSearchQuery with useCallback in TaskBoardView (line 194), OR
  2. Remove onSearchChange from deps with eslint-disable comment

🟢 Minor Improvements

4. Type Assertion Could Be Safer

Using as never (TaskCard.tsx:78) bypasses type checking. Define Badge variants as a type for proper compile-time safety.

5. Missing ARIA for Selection Mode

Screen reader users aren't notified when selection mode activates. Add aria-live region.

6. Backend: Missing Transaction Safety

Between get_task() and constructing TaskResponse (tasks_v2.py:235), task could be modified by another thread. Consider read locks for parallel batch execution scenarios.


🎯 Security Considerations

7. Rate Limiting on Batch Execution

The /api/v2/tasks/execute endpoint should have rate limiting since batch execution is expensive (spawns multiple AI agents). Consider adding @rate_limit_ai decorator.

8. Input Validation on Task IDs

No validation on task_id format. Consider max length and UUID format validation to prevent memory exhaustion.


📊 Performance

9. SWR Revalidation Strategy

Current uses default SWR settings. For frequently-changing task boards, consider:

  • refreshInterval: 5000 for polling
  • revalidateOnFocus: true
  • Or integrate with Phase 2 SSE streaming for push updates

✨ Nice-to-Haves (Future)

  • Drag-and-drop between columns
  • Bulk status changes
  • Keyboard shortcuts (e for execute, r for mark ready)
  • Task count badges on filter pills
  • Empty state illustrations

🎓 Commendations

  • Fix for structured error objects: The normalizeErrorDetail enhancement elegantly handles three error formats
  • Accessibility commitment: sr-only DialogTitle shows attention to WCAG compliance
  • Test-driven approach: 46 tests covering edge cases is exemplary
  • Clean state management: React hooks done right, no over-engineering
  • Responsive design: Adapts from 1 column (mobile) to 6 columns (desktop)

Summary

Overall Grade: A-

This is high-quality, production-ready code. The critical race condition should be fixed before merge (simple one-line fix). Architecture follows Phase 3 guidelines perfectly.

Recommendation: ✅ Approve with minor fixes

Fix the race condition cleanup, preserve selection on batch error, and this is ready to merge. Great work - this sets a strong foundation for Phase 3 web UI rebuild! 🚀

@frankbria frankbria merged commit 3ca940b into main Feb 6, 2026
14 checks passed
@frankbria frankbria deleted the feat/331-task-board-view branch February 6, 2026 03:54
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.

[Phase 3] Task Board View - Kanban & Batch Execution

1 participant