# Implementation Plan: Call List Feature ## Overview This plan implements the Call List feature for OrgMyLife — a dedicated phone-call tracking system with a new `CallItem` model, CRUD API, phone number extraction service, frontend "📞 Calls" tab, and Daily Digest integration. Tasks are ordered so each step builds on the previous, with no orphaned code. ## Tasks - [x] 1. Database model and migration - [x] 1.1 Add CallItem model to `app/models.py` - Add `CallItem` SQLAlchemy class with fields: id, person, reason, phone_number, original_task_id (FK → tasks.id), priority, status, created_at - Follow existing model patterns (Column types, server_default for timestamps) - _Requirements: 3.1, 3.2, 3.3_ - [x] 1.2 Create Alembic migration `005_add_call_items.py` - Create `call_items` table with all columns, FK constraint, and indexes on id and status - Include `downgrade()` that drops the table and indexes - _Requirements: 3.1, 3.2_ - [x] 2. Phone extractor service - [x] 2.1 Create `app/services/phone_extractor.py` - Implement `extract_phone_number(text: str) -> str | None` with regex patterns for: +49 international, 0xxx domestic, (0xxx) parenthesized area code - Implement `normalize_phone_number(raw: str) -> str` to strip separators and standardize - Implement `parse_phone_number(text: str) -> dict | None` to decompose into country_code, area_code, subscriber - Return first match when multiple numbers found; return None when no match - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ - [ ]* 2.2 Write property test: Phone number extraction (Property 2) - **Property 2: Phone number extraction from German formats** - Generate random valid German phone numbers in +49, 0xxx, and (0xxx) formats embedded in arbitrary text - Assert `extract_phone_number` returns a non-null result matching the first occurrence - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_phone_extractor.py` - **Validates: Requirements 4.1, 4.2, 4.3, 4.4** - [ ]* 2.3 Write property test: Phone number round-trip (Property 3) - **Property 3: Phone number parse-format round-trip** - Generate valid German phone number strings, parse → normalize → parse again - Assert the two parse results are equivalent - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_phone_extractor.py` - **Validates: Requirements 4.6** - [x] 3. Call List API routes and service logic - [x] 3.1 Add Pydantic models for Call List in `app/main.py` - `CallItemCreate`: task_id (optional), person (optional), reason (optional), phone_number (optional), priority (default 3) - `CallItemUpdate`: phone_number, priority, status, resolve_task (bool) - `CallItemResponse`: id, person, reason, phone_number, original_task_id, priority, status, created_at - Implement field truncation (person≤200, reason≤500, phone_number≤30) and priority clamping [1,4] in `model_post_init` - _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6_ - [x] 3.2 Implement POST `/api/calls` endpoint - If `task_id` provided: load task, extract sender name from description, extract phone number via `phone_extractor`, create CallItem with person=sender, reason=task.title, original_task_id=task.id - If no `task_id`: require person and reason fields, return 422 if missing - Assign default priority=3, status="pending" - Require session auth (401 if unauthenticated) - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.1, 2.5_ - [x] 3.3 Implement GET `/api/calls` endpoint - Return all CallItems with status="pending", ordered by priority ASC then created_at ASC - Require session auth - _Requirements: 2.2, 2.5_ - [x] 3.4 Implement PUT `/api/calls/{id}` endpoint - Update specified fields (phone_number, priority, status) - Validate status is "pending" or "done" only; reject others with 400 - If status="done" and resolve_task=true and original_task_id exists: mark original task as "completed" and trigger existing side effects (email archiving, Nextcloud sync) - If status="done" and resolve_task=false: only update CallItem - Return 404 if ID not found; require session auth - _Requirements: 2.3, 2.5, 2.6, 3.3, 8.1, 8.2, 8.3_ - [x] 3.5 Implement DELETE `/api/calls/{id}` endpoint - Remove CallItem from database - Return 404 if ID not found; require session auth - _Requirements: 2.4, 2.5, 2.6_ - [ ]* 3.6 Write property test: Field truncation (Property 5) - **Property 5: Field length truncation** - Generate strings longer than max lengths for person, reason, phone_number - Assert stored values are truncated to 200, 500, 30 chars respectively and equal the first N chars of input - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_call_list.py` - **Validates: Requirements 9.1, 9.2, 9.3** - [ ]* 3.7 Write property test: Priority clamping (Property 6) - **Property 6: Priority clamping** - Generate arbitrary integers, assert stored priority is clamped to [1, 4] - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_call_list.py` - **Validates: Requirements 9.4** - [ ]* 3.8 Write property test: Status constraint (Property 7) - **Property 7: Status constraint** - Generate arbitrary status strings, assert only "pending" and "done" are accepted - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_call_list.py` - **Validates: Requirements 3.3** - [x] 4. Checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. - [x] 5. Frontend: Calls tab and phone icon on tasks - [x] 5.1 Add "📞 Calls" navigation button to `frontend/index.html` - Insert new nav button between "Projects" and "Daily Digest" - Add `#calls-view` section with call items list container - _Requirements: 5.1_ - [x] 5.2 Implement Calls tab view in `frontend/js/app.js` - Add `loadCalls()` function: fetch GET `/api/calls`, render list with person, reason, phone (as `tel:` link), priority badge - Add "Mark Done" button per item (sends PUT with status="done", prompts about resolving original task if original_task_id exists) - Add "Delete" button per item (sends DELETE) - Add inline edit for phone_number field (sends PUT on blur/enter) - Order items by priority then creation date - Wire "📞 Calls" nav button to `switchTab("calls")` - _Requirements: 5.2, 5.3, 5.4, 5.5, 5.6, 5.7_ - [x] 5.3 Add 📞 icon button to task items in Task List view - Add phone icon button to existing task action buttons in `loadAllTasks()` render - On click: send POST `/api/calls` with `{ task_id: }` - Show confirmation indicator (brief flash or text) on success - _Requirements: 6.1, 6.2, 6.3_ - [x] 5.4 Add CSS styles for Calls view in `frontend/css/style.css` - Style call item cards, tel: links, inline phone edit, priority badges - Match existing card/list styling patterns - _Requirements: 5.2, 5.3_ - [x] 6. Daily Digest integration - [x] 6.1 Extend `generate_daily_digest()` in `app/services/digest.py` - Import CallItem model - Query all CallItems with status="pending" - Add `"pending_calls"` section to response dict with person and reason for each item - Omit section if no pending calls exist - _Requirements: 7.1, 7.2, 7.3_ - [x] 6.2 Render pending calls in Digest frontend view - In `loadDigest()`, render the `pending_calls` section when present in API response - Display as a "📞 Calls to make" card with person name and reason per item - _Requirements: 7.1, 7.2_ - [ ]* 6.3 Write property test: Digest includes all pending calls (Property 8) - **Property 8: Digest includes all pending calls** - Generate a non-empty set of pending CallItems, run `generate_daily_digest` - Assert response contains `pending_calls` with an entry for each pending item (person + reason) - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_call_digest.py` - **Validates: Requirements 7.1, 7.2** - [x] 7. Create-from-task logic and completion flow tests - [ ]* 7.1 Write property test: Create-from-task field mapping (Property 1) - **Property 1: Create-from-task field mapping** - Generate tasks with origin "gmail"/"dhive_email" and description starting with "From: {name}" - Assert created CallItem has: person=extracted sender, reason=task.title, original_task_id=task.id, priority=3, status="pending" - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_call_list.py` - **Validates: Requirements 1.1, 1.2, 1.3, 1.5** - [ ]* 7.2 Write property test: Call list ordering (Property 4) - **Property 4: Call list ordering** - Generate sets of pending CallItems with varying priorities and creation dates - Assert GET `/api/calls` returns items sorted by priority ASC, then created_at ASC - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_call_list.py` - **Validates: Requirements 2.2** - [ ]* 7.3 Write property test: Completion without task resolution (Property 9) - **Property 9: Call completion without task resolution** - Generate CallItems with original_task_id, mark as done with resolve_task=false - Assert only CallItem status changes; original task status remains unchanged - Use Hypothesis with `@settings(max_examples=100)` - Place in `OrgMyLife/tests/test_call_list.py` - **Validates: Requirements 8.3** - [ ]* 7.4 Write unit tests for API edge cases - Test 401 for unauthenticated requests to all /api/calls endpoints - Test 404 for non-existent call item IDs - Test 422 for POST without person/reason (when no task_id) - Test DELETE removes item from database - Test phone extractor returns None for text without numbers - Place in `OrgMyLife/tests/test_call_list.py` - _Requirements: 2.5, 2.6, 4.5, 9.5, 9.6_ - [x] 8. Final checkpoint - Ensure all tests pass - Ensure all tests pass, ask the user if questions arise. ## Notes - Tasks marked with `*` are optional and can be skipped for faster MVP - Each task references specific requirements for traceability - Checkpoints ensure incremental validation - Property tests validate universal correctness properties from the design document (Hypothesis, min 100 examples) - Unit tests validate specific examples and edge cases - The project uses Python (FastAPI + SQLAlchemy + Alembic) for backend and vanilla HTML/CSS/JS for frontend - Test files: `tests/test_phone_extractor.py`, `tests/test_call_list.py`, `tests/test_call_digest.py`