Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added (October 2025)

#### UI/UX Improvements (PR #76 - 2025-10-18)
- **Time Expectation Messages**: Added "This usually takes 1-2 minutes" messaging during summary generation
- Clear time expectations in summary generation modal dialog
- Compact "⏱ Takes 1-2 min" message in bottom overlay progress indicator
- Smart conditional rendering only for summary generation jobs
- Theme-aware, non-intrusive design
- **Mobile UX Enhancements**: Sticky bottom action bar for mobile edit/create modes
- **Enhanced Item Detail Panels**: Improved mobile support across task, risk, blocker, and lesson panels

#### Email Digest System (PR #54 - 2025-10-12)
- SendGrid integration for automated email delivery
- User email preferences management (digest frequency, content types)
Expand Down Expand Up @@ -101,6 +110,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

#### UI/UX (PR #76 - 2025-10-18)
- **Tasks & Lessons Screens**: Fixed tab highlighting and hid mobile navigation for cleaner mobile UX
- **Risks Screen**: Removed redundant Critical filter chip from navigation
- **Summary Generation Dialog**: Refactored for better error handling and user feedback
- **Navigation**: Improved filter displays and interactions across multiple screens

#### Backend (PR #76 - 2025-10-18)
- **Summary Service**: Refactored summary generation service with enhanced error handling
- **WebSocket Jobs**: Improved job status updates and progress tracking
- **Queue Configuration**: Enhanced RQ queue configuration for better reliability

#### Architecture
- Migrated from custom job service to Redis Queue (RQ) for background task processing (PR #40)
- Migrated from SnackBar to centralized NotificationService across entire app (PR #53)
Expand Down Expand Up @@ -138,6 +158,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

#### Frontend (PR #76 - 2025-10-18)
- **Test Fixes**: Fixed failing tests in lessons_learned_screen_v2_test.dart and query_provider_test.dart
- Fixed category tabs test to account for mobile layout behavior (11 tests passing)
- Fixed createNewSession test to match backend session ID assignment behavior (27 tests passing)
- **Ask AI Panel**: Eliminated duplicate conversation history entries
- **Query Provider**: Refactored to reduce code complexity (113 lines removed, 38 added)
- **Mobile Layout**: Fixed tab highlighting and navigation issues on mobile viewports

#### Backend
- Language validation and improved error handling in transcription API (PR #39)
- Backend test failures in error handling (PR #38)
Expand Down Expand Up @@ -350,6 +378,8 @@ If you're upgrading from an earlier version or different setup:

This changelog is based on the following merged pull requests:

- PR #76: UI improvements and test fixes (in progress)
- PR #75: Comprehensive documentation improvements for HLD, CHANGELOG, and README
- PR #74: Mobile UX improvements for item detail panels
- PR #73: Quality improvements for meeting upload processing
- PR #72: Enhanced right panel UX with text selection and truncation
Expand Down
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,33 @@ Contains 35 detailed development tasks with:
- **API tests**: Backend testing with pytest (when implemented)
- **Target coverage**: 80% for Flutter widgets

## MCP Servers

### Dart and Flutter MCP Server

The official Dart and Flutter MCP server is configured for this project, providing AI-powered development assistance.

**Status**: ✓ Connected (local scope)

**What it provides**:
- Get current runtime errors from running applications
- Search pub.dev for packages and add them as dependencies
- Generate widget code and self-correct syntax errors
- Access to Dart/Flutter SDK tools and diagnostics

**When to use**:
- Use the IDE MCP tools (`mcp__ide__getDiagnostics`, `mcp__ide__executeCode`) for accessing Flutter/Dart diagnostics and code execution
- The MCP server runs automatically in the background when using Claude Code
- Diagnostics are available for any Dart/Flutter files in the project

**Requirements**:
- Dart SDK 3.9+ / Flutter 3.35+ (✓ Currently: Dart 3.9.2, Flutter 3.35.6)

**Configuration**:
- Configured via: `claude mcp add dart --scope local -- dart mcp-server`
- Verify status: `claude mcp list`
- Config file: `~/.claude.json` (local scope for this project)

## Important Notes

1. **Project Phase**: Early development - currently only starter template exists
Expand Down
6 changes: 6 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ class Settings(BaseSettings):
fallback_on_overload: bool = Field(default=True, env="FALLBACK_ON_OVERLOAD") # Fallback on 529/503
fallback_on_rate_limit: bool = Field(default=False, env="FALLBACK_ON_RATE_LIMIT") # Don't fallback on 429 by default

# Manual Summary Generation LLM Configuration
# Override LLM settings for manual summary generation (project/program/portfolio summaries)
manual_summary_llm_provider: str = Field(default="claude", env="MANUAL_SUMMARY_LLM_PROVIDER")
manual_summary_llm_model: str = Field(default="claude-3-5-haiku-latest", env="MANUAL_SUMMARY_LLM_MODEL")
manual_summary_max_tokens: int = Field(default=8192, env="MANUAL_SUMMARY_MAX_TOKENS") # Claude 3.5 Haiku limit

# Circuit Breaker Configuration
enable_circuit_breaker: bool = Field(default=True, env="ENABLE_CIRCUIT_BREAKER")
circuit_breaker_failure_threshold: int = Field(default=5, env="CIRCUIT_BREAKER_FAILURE_THRESHOLD") # Open circuit after N consecutive failures
Expand Down
15 changes: 14 additions & 1 deletion backend/queue_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,20 @@ def publish_job_update(self, job_id: str, update_data: dict):
channel = f"job_updates:{job_id}"
message = json.dumps(update_data)
redis_conn.publish(channel, message)
logger.debug(f"Published update for job {job_id} to channel {channel}")

# Log job updates (use DEBUG to avoid production log pollution)
if update_data.get('status') == 'completed':
result_info = ""
if 'result' in update_data:
result_type = type(update_data['result']).__name__
result_keys = list(update_data['result'].keys()) if isinstance(update_data['result'], dict) else None
result_info = f", result_type={result_type}, result_keys={result_keys}"
logger.debug(
f"Published COMPLETED job update: job_id={job_id}, channel={channel}, "
f"update_keys={list(update_data.keys())}{result_info}"
)
else:
logger.debug(f"Published update for job {job_id} to channel {channel}")
except Exception as e:
logger.error(f"Failed to publish job update for {job_id}: {e}")

Expand Down
121 changes: 56 additions & 65 deletions backend/routers/unified_summaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel, Field
from typing import Optional, List, Literal
from typing import Optional, List, Literal, Union
from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
Expand All @@ -24,6 +24,13 @@
logger = get_logger(__name__)


class JobQueuedResponse(BaseModel):
"""Response when summary generation is queued as a background job."""
job_id: str = Field(..., description="RQ job ID for tracking generation progress")
status: str = Field(..., description="Job status (always 'processing' when queued)")
message: str = Field(..., description="Human-readable message")


class UnifiedSummaryRequest(BaseModel):
"""Unified request model for all summary generation."""
entity_type: Literal["project", "program", "portfolio"] = Field(..., description="Type of entity")
Expand All @@ -34,7 +41,6 @@ class UnifiedSummaryRequest(BaseModel):
date_range_end: Optional[datetime] = Field(None, description="End date for summaries")
format: Optional[str] = Field("general", description="Summary format: 'general', 'executive', 'technical', or 'stakeholder'")
created_by: Optional[str] = Field(None, description="User who requested the summary")
use_job: Optional[bool] = Field(False, description="Use job-based async generation")


class UnifiedSummaryResponse(BaseModel):
Expand Down Expand Up @@ -82,12 +88,39 @@ class SummaryFilters(BaseModel):

@router.post(
"/generate",
response_model=UnifiedSummaryResponse,
response_model=Union[JobQueuedResponse, UnifiedSummaryResponse],
summary="Generate Unified Summary",
description="Generate a summary for any entity type (project, program, portfolio). "
"This is the preferred endpoint for all summary generation operations.",
"Manual summaries (project/program/portfolio) are always queued as background jobs. "
"Meeting summaries are generated directly during content upload.",
responses={
200: {"description": "Summary generated successfully"},
200: {
"description": "Summary generation job queued or meeting summary generated",
"content": {
"application/json": {
"examples": {
"job_queued": {
"summary": "Manual summary generation (project/program/portfolio)",
"value": {
"job_id": "088c16d7-e5ad-447d-98c4-b9183cc2a0be",
"status": "processing",
"message": "Summary generation job queued successfully"
}
},
"meeting_summary": {
"summary": "Meeting summary (generated during upload)",
"value": {
"summary_id": "9c82c4b3-...",
"entity_type": "project",
"summary_type": "MEETING",
"subject": "Meeting Summary",
"body": "..."
}
}
}
}
}
},
400: {"description": "Invalid request parameters"},
404: {"description": "Entity not found"},
500: {"description": "Internal server error"}
Expand All @@ -103,20 +136,20 @@ async def generate_summary(
Generate a summary for any entity type (project, program, portfolio).

This unified endpoint handles all summary generation operations:
- **Meeting summaries**: For individual meeting content
- **Weekly summaries**: For project progress over time
- **Program summaries**: Aggregated insights from multiple projects
- **Portfolio summaries**: High-level insights from multiple programs
- **Meeting summaries**: Generated directly during content upload (synchronous)
- **Project summaries**: For project progress over time (job-based)
- **Program summaries**: Aggregated insights from multiple projects (job-based)
- **Portfolio summaries**: High-level insights from multiple programs (job-based)

**Request Parameters:**
- `entity_type`: The type of entity (project, program, portfolio)
- `entity_id`: UUID of the entity
- `summary_type`: Type of summary (meeting, weekly, program, portfolio)
- `summary_type`: Type of summary (meeting, project, program, portfolio)
- `format`: Output format (general, executive, technical, stakeholder)
- `use_job`: Whether to use background job processing

**Response:**
Returns either the generated summary or job information if `use_job=true`.
- For manual summaries (project/program/portfolio): Returns job ID for tracking
- For meeting summaries: Returns the completed summary (generated during upload)
"""
logger.info(f"Generating {sanitize_for_log(request.summary_type)} summary for {sanitize_for_log(request.entity_type)} {sanitize_for_log(request.entity_id)}")

Expand Down Expand Up @@ -171,8 +204,8 @@ async def generate_summary(
if request.date_range_end.tzinfo is not None:
request.date_range_end = request.date_range_end.replace(tzinfo=None)

# Handle job-based generation for long-running summaries
if request.use_job and request.summary_type in ["project", "program", "portfolio"]:
# Manual summaries (project/program/portfolio) always use job-based generation
if request.summary_type in ["project", "program", "portfolio"]:
# Enqueue RQ task for summary generation
from tasks.summary_tasks import generate_summary_task

Expand Down Expand Up @@ -207,22 +240,14 @@ async def generate_summary(

logger.info(f"Enqueued summary generation (RQ job: {rq_job.id})")

# Return job response with RQ job ID
return UnifiedSummaryResponse(
summary_id=rq_job.id, # Return RQ job ID directly
entity_type=request.entity_type,
entity_id=str(entity_uuid),
entity_name=entity_name,
project_id=None, # Added for Flutter model compatibility
summary_type=request.summary_type.upper(), # Convert to uppercase for Flutter enum
subject=f"Generating {request.summary_type} summary...",
body="Summary generation in progress. Check job status for updates.",
format=request.format,
created_at=datetime.now().isoformat(),
created_by=request.created_by
# Return job response
return JobQueuedResponse(
job_id=rq_job.id,
status="processing",
message="Summary generation job queued successfully"
)

# Direct generation
# Meeting summaries are generated directly (during content upload)
summary_data = None

if request.summary_type == "meeting":
Expand All @@ -243,43 +268,9 @@ async def generate_summary(
created_by_id=str(current_user.id),
format_type=request.format
)

elif request.summary_type == "project":
if request.entity_type == "project":
summary_data = await summary_service.generate_project_summary(
session=session,
project_id=entity_uuid,
week_start=request.date_range_start,
week_end=request.date_range_end,
created_by=current_user.email,
created_by_id=str(current_user.id),
format_type=request.format
)
else:
# For programs and portfolios, aggregate their children
raise HTTPException(status_code=501, detail="Project summaries for programs/portfolios not yet implemented")

elif request.summary_type == "program":
summary_data = await summary_service.generate_program_summary(
session=session,
program_id=entity_uuid,
week_start=request.date_range_start,
week_end=request.date_range_end,
created_by=current_user.email,
created_by_id=str(current_user.id),
format_type=request.format
)

elif request.summary_type == "portfolio":
summary_data = await summary_service.generate_portfolio_summary(
session=session,
portfolio_id=entity_uuid,
week_start=request.date_range_start,
week_end=request.date_range_end,
created_by=current_user.email,
created_by_id=str(current_user.id),
format_type=request.format
)
else:
# This should not happen - all other types use job-based generation
raise HTTPException(status_code=400, detail=f"Unsupported summary type: {request.summary_type}")

# Convert to response - Include project_id for compatibility with Flutter model
response_data = {
Expand Down
8 changes: 7 additions & 1 deletion backend/routers/websocket_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ def _get_rq_job_data(self, rq_job_id: str) -> Optional[dict]:
pass

# Build job data from RQ meta
# For result, prefer meta['result'] (set during task execution) over rq_job.result (set after task completes)
# Use explicit None check to allow falsy results (empty dict, 0, False, etc.)
result = meta.get('result')
if result is None and rq_job.is_finished:
result = rq_job.result

return {
'job_id': rq_job_id,
'project_id': meta.get('project_id', ''),
Expand All @@ -218,7 +224,7 @@ def _get_rq_job_data(self, rq_job_id: str) -> Optional[dict]:
'step_description': step_description,
'filename': meta.get('filename'),
'error_message': meta.get('error'),
'result': rq_job.result if rq_job.is_finished else None,
'result': result,
'created_at': rq_job.created_at.isoformat() if rq_job.created_at else None,
'updated_at': datetime.utcnow().isoformat(),
'metadata': meta
Expand Down
Loading
Loading