Initial monorepo structure
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"specId": "e33fa6e5-89e0-4d8d-ad9e-d43d6bf23daa", "workflowType": "requirements-first", "specType": "feature"}
|
||||
@@ -0,0 +1,343 @@
|
||||
# Design Document: Call List Feature
|
||||
|
||||
## Overview
|
||||
|
||||
The Call List feature adds a dedicated phone-call tracking system to OrgMyLife. It introduces a new `CallItem` model, a CRUD REST API at `/api/calls`, a phone number extraction service for German formats, a new "📞 Calls" frontend tab, and integration with the existing Daily Digest.
|
||||
|
||||
The design follows existing OrgMyLife patterns: SQLAlchemy model in `app/models.py`, FastAPI routes in `app/main.py`, service logic in `app/services/`, and vanilla JS frontend in `frontend/js/app.js`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Frontend
|
||||
A[Task List View] -->|📞 button click| B[POST /api/calls]
|
||||
C[Calls Tab] -->|load| D[GET /api/calls]
|
||||
C -->|mark done| E[PUT /api/calls/:id]
|
||||
C -->|delete| F[DELETE /api/calls/:id]
|
||||
end
|
||||
|
||||
subgraph Backend API
|
||||
B --> G[Call List Service]
|
||||
D --> G
|
||||
E --> G
|
||||
F --> G
|
||||
end
|
||||
|
||||
subgraph Services
|
||||
G --> H[Phone Extractor]
|
||||
G --> I[Task Completion Flow]
|
||||
J[Daily Digest Service] --> G
|
||||
end
|
||||
|
||||
subgraph Database
|
||||
G --> K[(call_items table)]
|
||||
I --> L[(tasks table)]
|
||||
end
|
||||
```
|
||||
|
||||
### Request Flow: Create Call from Task
|
||||
|
||||
1. User clicks 📞 on a task in the Task List
|
||||
2. Frontend sends `POST /api/calls` with `{ task_id: <id> }`
|
||||
3. Backend loads the task, extracts sender name from description
|
||||
4. Phone Extractor scans the task description for German phone numbers
|
||||
5. CallItem is created with person, reason, phone_number, original_task_id
|
||||
6. Response returns the created CallItem
|
||||
|
||||
### Request Flow: Complete Call with Task Resolution
|
||||
|
||||
1. User clicks "Done" on a call item that has `original_task_id`
|
||||
2. Frontend sends `PUT /api/calls/:id` with `{ status: "done", resolve_task: true }`
|
||||
3. Backend marks CallItem as done
|
||||
4. If `resolve_task` is true, backend marks the original task as "completed" and triggers existing side effects (email archiving, Nextcloud sync)
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### 1. CallItem Model (`app/models.py`)
|
||||
|
||||
New SQLAlchemy model added to the existing models file:
|
||||
|
||||
```python
|
||||
class CallItem(Base):
|
||||
__tablename__ = "call_items"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
person = Column(String(200), nullable=False)
|
||||
reason = Column(String(500), nullable=False)
|
||||
phone_number = Column(String(30), nullable=True)
|
||||
original_task_id = Column(Integer, ForeignKey("tasks.id"), nullable=True)
|
||||
priority = Column(Integer, default=3) # 1=Very High, 2=High, 3=Medium, 4=Low
|
||||
status = Column(String, default="pending") # "pending" or "done"
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
```
|
||||
|
||||
### 2. Phone Extractor Service (`app/services/phone_extractor.py`)
|
||||
|
||||
Pure function module with no external dependencies:
|
||||
|
||||
```python
|
||||
def extract_phone_number(text: str) -> str | None:
|
||||
"""Extract the first German phone number from text.
|
||||
|
||||
Supports formats:
|
||||
- +49 xxx xxxx xxxx (international)
|
||||
- 0xxx xxxxxxx (domestic with area code)
|
||||
- (0xxx) xxxxxxx (domestic with parenthesized area code)
|
||||
|
||||
Returns the first match or None.
|
||||
"""
|
||||
|
||||
def normalize_phone_number(raw: str) -> str:
|
||||
"""Normalize a phone number string by removing separators and standardizing format."""
|
||||
|
||||
def parse_phone_number(text: str) -> dict | None:
|
||||
"""Parse a phone number string into components: country_code, area_code, subscriber."""
|
||||
```
|
||||
|
||||
### 3. Call List API Routes (`app/main.py`)
|
||||
|
||||
New endpoints following existing patterns (session auth, Pydantic models):
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/calls` | Create call item (from task or manual) |
|
||||
| GET | `/api/calls` | List pending call items |
|
||||
| PUT | `/api/calls/{id}` | Update call item (status, phone, priority) |
|
||||
| DELETE | `/api/calls/{id}` | Remove call item |
|
||||
|
||||
### 4. Pydantic Request/Response Models
|
||||
|
||||
```python
|
||||
class CallItemCreate(BaseModel):
|
||||
task_id: Optional[int] = None # Create from existing task
|
||||
person: Optional[str] = None # Required if no task_id
|
||||
reason: Optional[str] = None # Required if no task_id
|
||||
phone_number: Optional[str] = None
|
||||
priority: Optional[int] = 3
|
||||
|
||||
class CallItemUpdate(BaseModel):
|
||||
phone_number: Optional[str] = None
|
||||
priority: Optional[int] = None
|
||||
status: Optional[str] = None # "pending" or "done"
|
||||
resolve_task: Optional[bool] = False # Also complete original task?
|
||||
|
||||
class CallItemResponse(BaseModel):
|
||||
id: int
|
||||
person: str
|
||||
reason: str
|
||||
phone_number: Optional[str]
|
||||
original_task_id: Optional[int]
|
||||
priority: int
|
||||
status: str
|
||||
created_at: str
|
||||
```
|
||||
|
||||
### 5. Daily Digest Integration
|
||||
|
||||
Extend `generate_daily_digest()` in `app/services/digest.py` to query pending CallItems and include a `"pending_calls"` section in the response dict.
|
||||
|
||||
### 6. Frontend Components
|
||||
|
||||
- **Navigation**: Add "📞 Calls" button between "Projects" and "Daily Digest"
|
||||
- **Calls View** (`#calls-view`): New section with call item list, inline phone edit
|
||||
- **Task Item**: Add 📞 button to existing task action buttons
|
||||
- **Digest View**: Render the `pending_calls` section when present
|
||||
|
||||
## Data Models
|
||||
|
||||
### CallItem Table Schema
|
||||
|
||||
| Column | Type | Constraints | Default |
|
||||
|--------|------|-------------|---------|
|
||||
| id | INTEGER | PRIMARY KEY, INDEX | auto |
|
||||
| person | VARCHAR(200) | NOT NULL | — |
|
||||
| reason | VARCHAR(500) | NOT NULL | — |
|
||||
| phone_number | VARCHAR(30) | NULLABLE | NULL |
|
||||
| original_task_id | INTEGER | FK → tasks.id, NULLABLE | NULL |
|
||||
| priority | INTEGER | — | 3 |
|
||||
| status | VARCHAR | — | "pending" |
|
||||
| created_at | DATETIME(tz) | — | CURRENT_TIMESTAMP |
|
||||
|
||||
### Alembic Migration
|
||||
|
||||
New migration file `005_add_call_items.py`:
|
||||
|
||||
```python
|
||||
def upgrade() -> None:
|
||||
op.create_table('call_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('person', sa.String(200), nullable=False),
|
||||
sa.Column('reason', sa.String(500), nullable=False),
|
||||
sa.Column('phone_number', sa.String(30), nullable=True),
|
||||
sa.Column('original_task_id', sa.Integer(), nullable=True),
|
||||
sa.Column('priority', sa.Integer(), server_default='3'),
|
||||
sa.Column('status', sa.String(), server_default="'pending'"),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)')),
|
||||
sa.ForeignKeyConstraint(['original_task_id'], ['tasks.id']),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_call_items_id'), 'call_items', ['id'], unique=False)
|
||||
op.create_index(op.f('ix_call_items_status'), 'call_items', ['status'], unique=False)
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f('ix_call_items_status'), table_name='call_items')
|
||||
op.drop_index(op.f('ix_call_items_id'), table_name='call_items')
|
||||
op.drop_table('call_items')
|
||||
```
|
||||
|
||||
### Relationship to Existing Models
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
tasks {
|
||||
int id PK
|
||||
string title
|
||||
text description
|
||||
int priority
|
||||
string status
|
||||
string origin
|
||||
string source_id
|
||||
}
|
||||
call_items {
|
||||
int id PK
|
||||
string person
|
||||
string reason
|
||||
string phone_number
|
||||
int original_task_id FK
|
||||
int priority
|
||||
string status
|
||||
datetime created_at
|
||||
}
|
||||
tasks ||--o{ call_items : "original_task_id"
|
||||
```
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||
|
||||
### Property 1: Create-from-task field mapping
|
||||
|
||||
*For any* valid Task with origin "gmail" or "dhive_email" and a description starting with "From: {name}", creating a CallItem from that task SHALL produce a CallItem where: person equals the extracted sender name, reason equals the task title, original_task_id equals the task ID, priority is 3, and status is "pending".
|
||||
|
||||
**Validates: Requirements 1.1, 1.2, 1.3, 1.5**
|
||||
|
||||
### Property 2: Phone number extraction from German formats
|
||||
|
||||
*For any* text string containing a valid German phone number (in +49, 0xxx, or (0xxx) format), the Phone_Extractor SHALL return a non-null result matching the first phone number occurrence in the text.
|
||||
|
||||
**Validates: Requirements 4.1, 4.2, 4.3, 4.4**
|
||||
|
||||
### Property 3: Phone number parse-format round-trip
|
||||
|
||||
*For any* valid German phone number string, parsing then formatting then parsing again SHALL produce an equivalent normalized result to parsing once.
|
||||
|
||||
**Validates: Requirements 4.6**
|
||||
|
||||
### Property 4: Call list ordering
|
||||
|
||||
*For any* set of pending CallItems with varying priorities and creation dates, the GET /api/calls response SHALL return items sorted by priority ascending, then by created_at ascending within the same priority.
|
||||
|
||||
**Validates: Requirements 2.2**
|
||||
|
||||
### Property 5: Field length truncation
|
||||
|
||||
*For any* string input to person (>200 chars), reason (>500 chars), or phone_number (>30 chars) fields, the stored value SHALL be truncated to the respective maximum length, and the stored value SHALL equal the first N characters of the input.
|
||||
|
||||
**Validates: Requirements 9.1, 9.2, 9.3**
|
||||
|
||||
### Property 6: Priority clamping
|
||||
|
||||
*For any* integer value provided as priority, the stored priority SHALL be clamped to the range [1, 4] — values below 1 become 1, values above 4 become 4.
|
||||
|
||||
**Validates: Requirements 9.4**
|
||||
|
||||
### Property 7: Status constraint
|
||||
|
||||
*For any* status value provided to a CallItem update, only "pending" and "done" SHALL be accepted; any other value SHALL be rejected or ignored.
|
||||
|
||||
**Validates: Requirements 3.3**
|
||||
|
||||
### Property 8: Digest includes all pending calls
|
||||
|
||||
*For any* non-empty set of CallItems with status "pending", the generated daily digest SHALL include a "pending_calls" section containing an entry for each pending item with its person name and reason.
|
||||
|
||||
**Validates: Requirements 7.1, 7.2**
|
||||
|
||||
### Property 9: Call completion without task resolution
|
||||
|
||||
*For any* CallItem with an original_task_id, marking it as "done" with resolve_task=false SHALL update only the CallItem status to "done" without modifying the Original_Task status.
|
||||
|
||||
**Validates: Requirements 8.3**
|
||||
|
||||
## Error Handling
|
||||
|
||||
### API Error Responses
|
||||
|
||||
| Scenario | HTTP Status | Response Body |
|
||||
|----------|-------------|---------------|
|
||||
| Unauthenticated request | 401 | `{"detail": "Not authenticated"}` |
|
||||
| CallItem not found | 404 | `{"detail": "Call item not found"}` |
|
||||
| Missing required field (person/reason) | 422 | Pydantic validation error |
|
||||
| Invalid status value | 400 | `{"detail": "Status must be 'pending' or 'done'"}` |
|
||||
| Referenced task_id not found | 404 | `{"detail": "Task not found"}` |
|
||||
|
||||
### Phone Extractor Error Handling
|
||||
|
||||
- Empty or None input → returns None (no exception)
|
||||
- Text with no phone numbers → returns None
|
||||
- Malformed numbers (too short/long) → skipped by regex, not returned
|
||||
- Non-string input → TypeError raised (caller responsibility)
|
||||
|
||||
### Task Resolution Errors
|
||||
|
||||
When completing a call with `resolve_task=true`:
|
||||
- If the original task no longer exists → log warning, complete call item only
|
||||
- If email archiving fails → log error, still mark task as completed locally
|
||||
- Follows the same error handling pattern as existing `update_task` endpoint
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Property-Based Tests (using Hypothesis)
|
||||
|
||||
The project uses Python, so **Hypothesis** is the property-based testing library. Each property test runs a minimum of 100 iterations.
|
||||
|
||||
Tests will be placed in `OrgMyLife/tests/test_call_list.py` and `OrgMyLife/tests/test_phone_extractor.py`.
|
||||
|
||||
**Property tests cover:**
|
||||
- Phone number extraction (Properties 2, 3)
|
||||
- Field truncation and priority clamping (Properties 5, 6)
|
||||
- Status constraint validation (Property 7)
|
||||
- Create-from-task field mapping (Property 1)
|
||||
- Call list ordering (Property 4)
|
||||
- Digest integration (Property 8)
|
||||
- Completion without resolution (Property 9)
|
||||
|
||||
Each test is tagged with: `# Feature: call-list, Property {N}: {description}`
|
||||
|
||||
Configuration: `@settings(max_examples=100)`
|
||||
|
||||
### Unit Tests (example-based)
|
||||
|
||||
- Authentication enforcement (401 for all endpoints)
|
||||
- 404 for non-existent IDs
|
||||
- DELETE removes item from database
|
||||
- POST without person/reason returns 422
|
||||
- Phone extractor returns None for text without numbers
|
||||
- Digest omits calls section when no pending items exist
|
||||
- Frontend renders tel: link for items with phone numbers
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Create call from task → verify task side effects (email archiving) on completion
|
||||
- Full flow: create from task → mark done with resolve_task=true → verify original task completed
|
||||
- Alembic migration up/down cycle
|
||||
|
||||
### Test File Structure
|
||||
|
||||
```
|
||||
OrgMyLife/tests/
|
||||
├── test_phone_extractor.py # Property tests for extraction + round-trip
|
||||
├── test_call_list.py # Property + unit tests for API and service logic
|
||||
└── test_call_digest.py # Property tests for digest integration
|
||||
```
|
||||
@@ -0,0 +1,122 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
The Call List feature adds a dedicated "📞 Calls" tab to OrgMyLife that acts as a separate funnel for phone-call tasks. Users can promote any existing task to the Call List, where it is tracked with call-specific metadata (person, phone number, reason). Phone numbers are auto-extracted from email bodies using German format patterns. The Call List integrates with the Daily Digest to surface pending calls.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Call_List_Service**: The backend service responsible for managing call items, including creation, retrieval, updates, and deletion via the REST API.
|
||||
- **Call_Item**: A database entity representing a phone call to be made, containing person name, reason, phone number, priority, status, and a reference to the originating task.
|
||||
- **Phone_Extractor**: The component that scans email body text and extracts phone numbers matching German telephone formats.
|
||||
- **Frontend_Call_View**: The UI tab ("📞 Calls") that displays pending and completed call items with sorting and action controls.
|
||||
- **Task_List_View**: The existing "All Tasks" UI view showing open tasks with action buttons.
|
||||
- **Daily_Digest_Service**: The existing service that generates the daily summary of overdue tasks, priorities, and schedule.
|
||||
- **Original_Task**: The existing Task entity from which a Call_Item is derived.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Create Call Item from Task
|
||||
|
||||
**User Story:** As a user, I want to move a task to the Call List by clicking a phone icon, so that I can track phone calls separately from regular tasks.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the user clicks the phone icon button on a task in the Task_List_View, THE Call_List_Service SHALL create a new Call_Item linked to the Original_Task.
|
||||
2. WHEN a Call_Item is created from an Original_Task, THE Call_List_Service SHALL populate the Call_Item person field with the task sender name if the origin is "gmail" or "dhive_email".
|
||||
3. WHEN a Call_Item is created from an Original_Task, THE Call_List_Service SHALL populate the Call_Item reason field with the Original_Task title.
|
||||
4. WHEN a Call_Item is created from an Original_Task that has an email body, THE Phone_Extractor SHALL scan the email body and populate the Call_Item phone_number field with the first extracted number.
|
||||
5. WHEN a Call_Item is created, THE Call_List_Service SHALL assign a default priority of 3 (Medium) and a status of "pending".
|
||||
|
||||
### Requirement 2: Call List REST API
|
||||
|
||||
**User Story:** As a developer, I want a complete CRUD API for call items, so that the frontend and external integrations can manage the Call List.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a POST request is sent to /api/calls with valid call item data, THE Call_List_Service SHALL create a new Call_Item and return the created item with its ID.
|
||||
2. WHEN a GET request is sent to /api/calls, THE Call_List_Service SHALL return all Call_Items with status "pending" ordered by priority then creation date.
|
||||
3. WHEN a PUT request is sent to /api/calls/{id} with update data, THE Call_List_Service SHALL update the specified Call_Item fields and return the updated item.
|
||||
4. WHEN a DELETE request is sent to /api/calls/{id}, THE Call_List_Service SHALL remove the specified Call_Item from the database.
|
||||
5. WHEN an unauthenticated request is sent to any /api/calls endpoint, THE Call_List_Service SHALL return HTTP 401.
|
||||
6. WHEN a request references a non-existent Call_Item ID, THE Call_List_Service SHALL return HTTP 404.
|
||||
|
||||
### Requirement 3: Call Item Data Model
|
||||
|
||||
**User Story:** As a developer, I want a dedicated CallItem model with call-specific fields, so that call metadata is stored separately from general tasks.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Call_Item SHALL store the following fields: id (integer, primary key), person (string, required), reason (string, required), phone_number (string, nullable), original_task_id (integer, nullable foreign key to tasks), priority (integer, default 3), status (string, default "pending"), created_at (timestamp with timezone).
|
||||
2. WHEN a Call_Item references an Original_Task, THE Call_Item original_task_id SHALL be a valid foreign key to the tasks table.
|
||||
3. THE Call_Item status field SHALL accept only the values "pending" and "done".
|
||||
|
||||
### Requirement 4: Phone Number Extraction from Email
|
||||
|
||||
**User Story:** As a user, I want phone numbers automatically extracted from email bodies, so that I do not have to manually look up contact numbers.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN an email body is provided, THE Phone_Extractor SHALL identify phone numbers in the format +49 followed by 9 to 12 digits with optional spaces or hyphens.
|
||||
2. WHEN an email body is provided, THE Phone_Extractor SHALL identify phone numbers in the format 0 followed by 3 to 5 digit area code and 4 to 8 digit subscriber number with optional separators.
|
||||
3. WHEN an email body is provided, THE Phone_Extractor SHALL identify phone numbers in the format (0xxx) followed by subscriber digits with optional separators.
|
||||
4. WHEN multiple phone numbers are found in an email body, THE Phone_Extractor SHALL return the first match.
|
||||
5. WHEN no phone number is found in an email body, THE Phone_Extractor SHALL return null.
|
||||
6. FOR ALL valid German phone number strings, parsing then formatting then parsing SHALL produce an equivalent normalized result (round-trip property).
|
||||
|
||||
### Requirement 5: Call List Frontend Tab
|
||||
|
||||
**User Story:** As a user, I want a dedicated "📞 Calls" tab in the navigation, so that I can view and manage my pending calls in one place.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Frontend_Call_View SHALL display a "📞 Calls" navigation button in the top navigation bar between "Projects" and "Daily Digest".
|
||||
2. WHEN the user navigates to the Calls tab, THE Frontend_Call_View SHALL display all pending Call_Items showing person name, reason, phone number, and priority.
|
||||
3. WHEN a Call_Item has a phone_number, THE Frontend_Call_View SHALL display the phone number as a clickable tel: link.
|
||||
4. THE Frontend_Call_View SHALL provide a button to mark a Call_Item as done.
|
||||
5. THE Frontend_Call_View SHALL provide a button to delete a Call_Item.
|
||||
6. THE Frontend_Call_View SHALL allow editing the phone_number field inline.
|
||||
7. THE Frontend_Call_View SHALL order Call_Items by priority (ascending) then by creation date (ascending).
|
||||
|
||||
### Requirement 6: Phone Icon on Task Items
|
||||
|
||||
**User Story:** As a user, I want a phone icon button on each task, so that I can quickly send any task to the Call List.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Task_List_View SHALL display a 📞 icon button on each task item alongside the existing action buttons.
|
||||
2. WHEN the user clicks the 📞 button on a task, THE Frontend_Call_View SHALL send a POST request to /api/calls with the task data.
|
||||
3. WHEN the Call_Item is successfully created from a task, THE Frontend_Call_View SHALL display a confirmation indicator to the user.
|
||||
|
||||
### Requirement 7: Daily Digest Integration
|
||||
|
||||
**User Story:** As a user, I want to see pending calls in my Daily Digest, so that I am reminded of calls I need to make today.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN the Daily Digest is generated, THE Daily_Digest_Service SHALL include a "📞 Calls to make" section listing all Call_Items with status "pending".
|
||||
2. WHEN there are pending Call_Items, THE Daily_Digest_Service SHALL display each item with person name and reason.
|
||||
3. WHEN there are no pending Call_Items, THE Daily_Digest_Service SHALL omit the calls section from the digest.
|
||||
|
||||
### Requirement 8: Complete Call and Resolve Original Task
|
||||
|
||||
**User Story:** As a user, I want the option to mark the original task as done when I complete a call, so that I do not have to update both items manually.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a Call_Item is marked as done and the Call_Item has an original_task_id, THE Call_List_Service SHALL offer the option to also mark the Original_Task as "completed".
|
||||
2. WHEN the user confirms marking the Original_Task as completed, THE Call_List_Service SHALL update the Original_Task status to "completed" and trigger the same side effects as the existing task completion flow (email archiving, Nextcloud sync).
|
||||
3. WHEN the user declines marking the Original_Task as completed, THE Call_List_Service SHALL only update the Call_Item status to "done" without modifying the Original_Task.
|
||||
|
||||
### Requirement 9: Input Validation
|
||||
|
||||
**User Story:** As a developer, I want call item inputs validated, so that the database contains only well-formed data.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a POST or PUT request is sent with a person field exceeding 200 characters, THE Call_List_Service SHALL truncate the value to 200 characters.
|
||||
2. WHEN a POST or PUT request is sent with a reason field exceeding 500 characters, THE Call_List_Service SHALL truncate the value to 500 characters.
|
||||
3. WHEN a POST or PUT request is sent with a phone_number field exceeding 30 characters, THE Call_List_Service SHALL truncate the value to 30 characters.
|
||||
4. WHEN a POST or PUT request is sent with a priority value outside the range 1 to 4, THE Call_List_Service SHALL clamp the value to the nearest valid bound.
|
||||
5. WHEN a POST request is sent without a person field, THE Call_List_Service SHALL return HTTP 422 with a validation error.
|
||||
6. WHEN a POST request is sent without a reason field, THE Call_List_Service SHALL return HTTP 422 with a validation error.
|
||||
@@ -0,0 +1,194 @@
|
||||
# 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: <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`
|
||||
Reference in New Issue
Block a user