Files
Orchestrator/.kiro/specs/call-list/design.md
2026-06-30 20:37:40 +02:00

13 KiB

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

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:

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:

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

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:

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

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