# Task Recon: Implement Progress Bar with Real-Time Success/Fail Counts

**Task ID:** 27.2
**Status:** pending
**Generated:** 2026-01-29
**Agent:** claude-opus-4-5-20251101

---

## Task Summary

Implement the bulk assignment processing loop with Rich Progress bar showing current/total, success count, and failure count that updates in real-time during execution. This subtask replaces the placeholder at line 1107-1112 of `numbers.py` with actual assignment execution logic.

### Parent Task Context

**Parent Task:** Implement Numbers Bulk-Assign Command (ID: 27)
**Parent Goal:** Add bulk-assign command to numbers CLI with progress bar, continue-on-error, and results file output. The command should read a CSV file, validate all rows, show progress during execution, and generate a results file.
**This Subtask's Role:** Handles the execution phase (Step 5 in the parent task's workflow) - actually processing validated rows with visual feedback via progress bar showing live success/failure counts.

### Dependencies
- Depends on: 27.1 (CSV Parsing and Validation Phase with Rich Summary Display) - DONE

### Acceptance Criteria
From task details:
1. Create custom progress bar using OutputFormatter.progress() context manager
2. Progress description format: `Processing [current/total] checkmark{success} x{fail}`
3. Initialize counters: success_count = 0, fail_count = 0
4. Create results list to collect BulkOperationResult for each row
5. For each validated row: call number_service.assign_number() with wait=True
6. On success: increment success_count, record result with status='success'
7. On failure: increment fail_count, record result with error details
8. Update progress bar description with current counts after each iteration
9. Advance progress bar by 1 after each row
10. Handle JSON mode: suppress progress bar display
11. Handle keyboard interrupt (Ctrl+C) gracefully with partial results

---

## Insights from Previous Tasks

### Dependency Task Notes
| Task ID | Title | Key Insight |
|---------|-------|-------------|
| 27.1 | CSV Parsing and Validation Phase | Exit codes: dry-run exits 0 (no errors) or 1 (errors); validation failure without --continue-on-error raises ValidationError (exit code 5) |
| 26 | Bulk CSV Parsing and Validation | BulkAssignResult dataclass has status values: 'success', 'failed', 'skipped'; multiple errors joined with "; " delimiter |
| 21 | Number Assign/Unassign Operations | assign_number() returns AsyncOperation; on_progress callback for polling status; wait=True polls for completion |
| 23 | Numbers Assign/Unassign CLI Commands | Progress spinner pattern with _run_with_progress(); confirmation prompt pattern; JSON mode suppresses spinner |

### Reusable Patterns from Dependencies
- Multi-service async helper pattern: `_run_bulk_assign_async` already exists at `numbers.py:130-162`
- Progress spinner pattern: `_run_with_progress()` at `numbers.py:165-204` - but this is for single operations, not bulk
- OutputFormatter.progress() context manager: `output_formatter.py:191-227` - yields Progress for bar display
- BulkAssignResult dataclass: `bulk_operations.py:61-78` - ready to use for results collection

### Warnings and Gotchas
- **From Task 27.1**: ANSI codes may be present in CLI output even with `--no-color` when testing - use substring assertions
- **From Task 26**: Caller must join multiple errors with `"; "` delimiter before passing to BulkAssignResult
- **From Task 21**: GraphClient uses public `graph_client` attribute, NOT `_graph_client` (private)
- **From Task 23**: When testing progress behavior, capture `on_progress` parameter in mock side_effect

### Open Items Inherited
- [ ] The `--output` option is accepted but not yet used (execution phase needed) - addressed in subtask 27.3
- [ ] Subtask 27.3 will implement error handling and results file output

---

## Architecture Context

### Relevant Design Decisions
- CLI Layer handles user interaction and formatting; Services handle business logic
- Graph assign operations return 202 and require polling via `poll_operation()`
- OutputFormatter manages JSON/human-readable output modes and progress bar suppression

### Module Structure
```
src/teams_phone/cli/numbers.py        # CLI commands, progress display
src/teams_phone/services/
  bulk_operations.py                   # BulkOperations class, BulkAssignResult
  number_service.py                    # assign_number() method
src/teams_phone/infrastructure/
  output_formatter.py                  # progress() context manager
```

### Conventions to Follow
- Use `wait=True` for synchronous operation completion during bulk processing
- Collect all results before final output (don't exit early on failure unless --continue-on-error is false)
- Progress bar hidden in JSON mode (OutputFormatter handles this automatically)
- Use try/except around individual operations to handle failures gracefully

---

## Related Code References

### Primary References (Must Read)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/numbers.py` | 1107-1112 | **Placeholder to replace** - current execution phase stub |
| `src/teams_phone/cli/numbers.py` | 165-204 | `_run_with_progress()` - reference for progress display pattern |
| `src/teams_phone/infrastructure/output_formatter.py` | 191-227 | `progress()` context manager - Progress bar API |
| `src/teams_phone/services/bulk_operations.py` | 61-78 | `BulkAssignResult` dataclass for results collection |
| `src/teams_phone/services/number_service.py` | 291-361 | `assign_number()` method signature and behavior |

### Secondary References (Helpful Context)
| File | Lines | Purpose |
|------|-------|---------|
| `src/teams_phone/cli/numbers.py` | 130-162 | `_run_bulk_assign_async()` - service initialization pattern |
| `src/teams_phone/cli/numbers.py` | 693-720 | Existing progress integration in assign command (spinner) |
| `tests/unit/test_cli_numbers.py` | 2091-2137 | Test pattern for --yes skipping confirmation |
| `src/teams_phone/models/number.py` | 76-93 | OperationStatus enum values |

### Code Patterns to Follow

```python
# Pattern 1: OutputFormatter.progress() context manager (output_formatter.py:191-227)
# Note: Returns Progress object, need to call add_task() and update()
@contextmanager
def progress(
    self,
    total: int,
    description: str = "Processing",
) -> Iterator[Progress]:
    """Return a Progress context manager for displaying progress bars."""
    if self.json_mode:
        # Return a minimal progress that does nothing in JSON mode
        progress = Progress(
            console=Console(force_terminal=False, quiet=True),
            disable=True,
        )
        with progress:
            progress.add_task(description, total=total)
            yield progress
    else:
        # Use ASCII-safe spinner on Windows legacy console
        spinner_name = self.get_spinner_name()
        progress = Progress(
            SpinnerColumn(spinner_name=spinner_name),
            TextColumn("[progress.description]{task.description}"),
            BarColumn(),
            TaskProgressColumn(),
            console=self.console,
        )
        with progress:
            progress.add_task(description, total=total)
            yield progress
```

```python
# Pattern 2: NumberService.assign_number() call (number_service.py:291-361)
async def assign_number(
    self,
    user_id: str,
    phone_number: str,
    number_type: NumberType,
    *,
    location_id: str | None = None,
    wait: bool = True,
    timeout: int = 60,
    on_progress: Callable[[AsyncOperation], None] | None = None,
) -> AsyncOperation:
```

```python
# Pattern 3: BulkAssignResult usage (bulk_operations.py:61-78)
@dataclass
class BulkAssignResult:
    row_number: int
    user: str
    phone_number: str
    status: str  # 'success' | 'failed' | 'skipped'
    error: str | None = None
```

---

## Dependencies

### Internal Modules
- `teams_phone.services.bulk_operations` - BulkAssignResult, BulkAssignRow
- `teams_phone.services.number_service` - NumberService.assign_number()
- `teams_phone.infrastructure.output_formatter` - OutputFormatter.progress()
- `teams_phone.models` - OperationStatus, NumberType, AsyncOperation
- `teams_phone.exceptions` - TeamsPhoneError for catching operation failures

### External Packages
- `rich.progress` - Progress, TaskID for progress bar updates (already imported via OutputFormatter)
- `asyncio` - for running async operations (already in use)

### New Dependencies Needed
- None required - all needed modules already imported or available

---

## Testing Guidance

### Test File Location
`tests/unit/test_cli_numbers.py` - Add tests to `TestNumbersBulkAssignCommand` class

### Test Patterns in Use
```python
# Example test structure from test_cli_numbers.py:2091
@pytest.mark.unit
def test_bulk_assign_yes_skips_confirmation(self, tmp_path: "Path") -> None:
    """bulk-assign --yes skips confirmation prompt."""
    csv_content = "user,phone_number,location\njohn@contoso.com,+14255551234,Seattle HQ\n"
    csv_file = tmp_path / "test.csv"
    csv_file.write_text(csv_content)

    # Create mock validation result - all valid
    mock_validation = MagicMock()
    mock_validation.rows = [MagicMock()]
    # ... setup mock properties ...

    with (
        patch("teams_phone.cli.numbers.CacheManager"),
        patch("teams_phone.cli.numbers.AuthService"),
        patch("teams_phone.cli.numbers.GraphClient") as mock_gc_class,
        patch("teams_phone.cli.numbers.UserService"),
        patch("teams_phone.cli.numbers.NumberService") as mock_ns_class,
        patch("teams_phone.cli.numbers.LocationService"),
        patch("teams_phone.cli.numbers.BulkOperations") as mock_bulk_class,
    ):
        # Setup mocks...
        mock_ns = MagicMock()
        mock_ns.assign_number = AsyncMock(return_value=mock_operation)
        mock_ns_class.return_value = mock_ns

        result = runner.invoke(
            app,
            ["--no-color", "numbers", "bulk-assign", str(csv_file), "--yes"],
        )
        assert result.exit_code == 0
```

### Fixtures Available
- `tmp_path` - pytest fixture for temporary file creation
- `runner` - Typer CliRunner from test file setup

### Test Utilities
- `tests/conftest.py` - Markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.contract
- `MagicMock`, `AsyncMock` - From unittest.mock for service mocking
- `patch` - From unittest.mock for patching module-level imports

### Tests to Write
1. `test_bulk_assign_progress_bar_updates_on_success` - Verify progress advances and success count increments
2. `test_bulk_assign_progress_bar_updates_on_failure` - Verify fail count increments on operation failure
3. `test_bulk_assign_json_mode_suppresses_progress` - Verify no progress output in JSON mode
4. `test_bulk_assign_keyboard_interrupt_saves_partial_results` - Test Ctrl+C handling (may be complex to test)
5. `test_bulk_assign_collects_results_for_all_rows` - Verify results list includes all processed rows
6. `test_bulk_assign_skips_rows_with_validation_errors` - Verify rows with errors are skipped during execution

---

## Risks and Blockers

### Technical Risks
- [ ] **Progress bar description update**: Need to verify that `progress.update(task_id, description=...)` works with the current OutputFormatter.progress() implementation - the context manager adds a task but doesn't return the task_id. May need to modify the implementation or access the Progress object differently.
- [ ] **Async operation within progress context**: The Progress context manager from OutputFormatter yields synchronously, but we need to run async assign_number() calls inside it. Need asyncio.run() or similar pattern.

### Dependency Risks
- [ ] None identified - all dependencies (27.1, 26, 21, 23) are complete

### Open Questions
- [ ] **Progress description format**: Task specifies `Processing [current/total] checkmark{success} x{fail}` - need to clarify exact format with Unicode/ASCII checkmark/x symbols. Consider using OutputFormatter's ASCII mode for Windows compatibility.
- [ ] **Results collection placement**: Where should results be stored after this subtask? Subtask 27.3 handles results file output, so this subtask should prepare but not write the results.

---

## Implementation Recommendations

### Suggested Approach
1. **Remove placeholder** at lines 1107-1112
2. **Create async helper function** for execution phase that takes validated rows and services
3. **Get task_id from Progress.add_task()** - modify approach since OutputFormatter.progress() already adds a task
4. **Implement processing loop**:
   - Initialize counters and results list
   - Use OutputFormatter.progress() context manager
   - Get task_id from progress bar (need to access Progress.tasks)
   - For each valid row (skip rows with errors):
     - Update description with current counts
     - Call assign_number() with wait=True
     - Handle success/failure, update counters
     - Create BulkAssignResult
     - Advance progress
5. **Handle keyboard interrupt** with try/except KeyboardInterrupt
6. **Store results** for subtask 27.3 to write to file

### Files to Create/Modify
| Action | File | Reason |
|--------|------|--------|
| Modify | `src/teams_phone/cli/numbers.py` | Replace placeholder with execution phase (lines 1107-1112) |
| Modify | `tests/unit/test_cli_numbers.py` | Add execution phase tests to TestNumbersBulkAssignCommand |

### Quick Wins
- Start by implementing the simplest case: process one row successfully with progress bar
- Use existing patterns from `_run_with_progress()` for progress display integration

---

## Cheat Sheet

### Key Imports
```python
# Already imported in numbers.py
from teams_phone.services.bulk_operations import (
    BulkOperations,
    BulkValidationResult,
    BulkAssignResult,  # ADD THIS IMPORT
    parse_assign_csv,
)
from teams_phone.models import (
    OperationStatus,
    NumberType,
    AsyncOperation,
)
from teams_phone.exceptions import TeamsPhoneError
```

### Key Functions to Use
- `formatter.progress(total, description)` - Returns Progress context manager
- `progress.update(task_id, description=..., advance=1)` - Update progress bar
- `number_service.assign_number(user_id, phone_number, number_type, location_id=..., wait=True)` - Execute assignment
- `BulkAssignResult(row_number, user, phone_number, status, error)` - Create result record

### Common Patterns
- Progress bar update: See `output_formatter.py:191-227` for context manager
- Operation status check: `if result.status == OperationStatus.SUCCEEDED:`
- Error handling: `except TeamsPhoneError as e: fail_count += 1; error = e.message`
- ASCII-safe checkmark/x: Use `formatter.placeholder` pattern or simple text like "OK"/"FAIL"

### Progress Description Format Suggestion
```python
# Instead of Unicode checkmark/x, use ASCII for Windows compatibility
description = f"Processing [{current}/{total}] OK:{success_count} FAIL:{fail_count}"
# Or if Unicode is acceptable:
description = f"Processing [{current}/{total}] \u2713{success_count} \u2717{fail_count}"
```

---

*Generated by task-recon (claude-opus-4-5-20251101) | Ready for implementation*
