draft-board 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Dashboard and metrics API endpoints."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, Query
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from sqlalchemy import func, select
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
+
|
|
11
|
+
from app.database import get_db
|
|
12
|
+
from app.models.agent_session import AgentSession
|
|
13
|
+
from app.models.ticket import Ticket
|
|
14
|
+
from app.state_machine import TicketState
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================================
|
|
22
|
+
# Response Models
|
|
23
|
+
# ============================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BudgetStatus(BaseModel):
|
|
27
|
+
"""Budget tracking status."""
|
|
28
|
+
|
|
29
|
+
daily_budget: float | None = None
|
|
30
|
+
daily_spent: float = 0
|
|
31
|
+
daily_remaining: float = 0
|
|
32
|
+
weekly_budget: float | None = None
|
|
33
|
+
weekly_spent: float = 0
|
|
34
|
+
weekly_remaining: float = 0
|
|
35
|
+
monthly_budget: float | None = None
|
|
36
|
+
monthly_spent: float = 0
|
|
37
|
+
monthly_remaining: float = 0
|
|
38
|
+
is_over_budget: bool = False
|
|
39
|
+
warning_threshold_reached: bool = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SprintMetrics(BaseModel):
|
|
43
|
+
"""Sprint progress metrics."""
|
|
44
|
+
|
|
45
|
+
total_tickets: int = 0
|
|
46
|
+
completed_tickets: int = 0
|
|
47
|
+
in_progress_tickets: int = 0
|
|
48
|
+
blocked_tickets: int = 0
|
|
49
|
+
completion_rate: float = 0
|
|
50
|
+
avg_cycle_time_hours: float = 0
|
|
51
|
+
velocity: float = 0 # tickets per day
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AgentMetrics(BaseModel):
|
|
55
|
+
"""AI agent usage metrics."""
|
|
56
|
+
|
|
57
|
+
total_sessions: int = 0
|
|
58
|
+
successful_sessions: int = 0
|
|
59
|
+
success_rate: float = 0
|
|
60
|
+
avg_turns_per_session: float = 0
|
|
61
|
+
most_used_agent: str = "claude"
|
|
62
|
+
total_cost_usd: float = 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CostTrendItem(BaseModel):
|
|
66
|
+
"""Daily cost trend item."""
|
|
67
|
+
|
|
68
|
+
date: str
|
|
69
|
+
cost: float
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DashboardResponse(BaseModel):
|
|
73
|
+
"""Complete dashboard data."""
|
|
74
|
+
|
|
75
|
+
budget: BudgetStatus
|
|
76
|
+
sprint: SprintMetrics
|
|
77
|
+
agent: AgentMetrics
|
|
78
|
+
cost_trend: list[CostTrendItem] = Field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ============================================================================
|
|
82
|
+
# Helper Functions
|
|
83
|
+
# ============================================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def get_period_cost(
|
|
87
|
+
db: AsyncSession,
|
|
88
|
+
start_date: datetime,
|
|
89
|
+
end_date: datetime,
|
|
90
|
+
goal_id: str | None = None,
|
|
91
|
+
) -> float:
|
|
92
|
+
"""Get total cost for a time period."""
|
|
93
|
+
query = select(func.coalesce(func.sum(AgentSession.estimated_cost_usd), 0)).where(
|
|
94
|
+
AgentSession.created_at >= start_date, AgentSession.created_at < end_date
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if goal_id:
|
|
98
|
+
query = query.join(Ticket).where(Ticket.goal_id == goal_id)
|
|
99
|
+
|
|
100
|
+
result = await db.execute(query)
|
|
101
|
+
return float(result.scalar() or 0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def get_budget_status(
|
|
105
|
+
db: AsyncSession,
|
|
106
|
+
goal_id: str | None = None,
|
|
107
|
+
daily_budget: float = 10.0,
|
|
108
|
+
weekly_budget: float = 50.0,
|
|
109
|
+
monthly_budget: float = 150.0,
|
|
110
|
+
) -> BudgetStatus:
|
|
111
|
+
"""Calculate budget status for all periods."""
|
|
112
|
+
now = datetime.utcnow()
|
|
113
|
+
|
|
114
|
+
# Daily
|
|
115
|
+
day_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
116
|
+
daily_spent = await get_period_cost(
|
|
117
|
+
db, day_start, day_start + timedelta(days=1), goal_id
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Weekly (Monday start)
|
|
121
|
+
week_start = day_start - timedelta(days=day_start.weekday())
|
|
122
|
+
weekly_spent = await get_period_cost(
|
|
123
|
+
db, week_start, week_start + timedelta(weeks=1), goal_id
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Monthly
|
|
127
|
+
month_start = day_start.replace(day=1)
|
|
128
|
+
if month_start.month == 12:
|
|
129
|
+
month_end = month_start.replace(year=month_start.year + 1, month=1)
|
|
130
|
+
else:
|
|
131
|
+
month_end = month_start.replace(month=month_start.month + 1)
|
|
132
|
+
monthly_spent = await get_period_cost(db, month_start, month_end, goal_id)
|
|
133
|
+
|
|
134
|
+
is_over = (
|
|
135
|
+
daily_spent > daily_budget
|
|
136
|
+
or weekly_spent > weekly_budget
|
|
137
|
+
or monthly_spent > monthly_budget
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
warning = (
|
|
141
|
+
daily_spent >= daily_budget * 0.8
|
|
142
|
+
or weekly_spent >= weekly_budget * 0.8
|
|
143
|
+
or monthly_spent >= monthly_budget * 0.8
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return BudgetStatus(
|
|
147
|
+
daily_budget=daily_budget,
|
|
148
|
+
daily_spent=round(daily_spent, 2),
|
|
149
|
+
daily_remaining=round(max(0, daily_budget - daily_spent), 2),
|
|
150
|
+
weekly_budget=weekly_budget,
|
|
151
|
+
weekly_spent=round(weekly_spent, 2),
|
|
152
|
+
weekly_remaining=round(max(0, weekly_budget - weekly_spent), 2),
|
|
153
|
+
monthly_budget=monthly_budget,
|
|
154
|
+
monthly_spent=round(monthly_spent, 2),
|
|
155
|
+
monthly_remaining=round(max(0, monthly_budget - monthly_spent), 2),
|
|
156
|
+
is_over_budget=is_over,
|
|
157
|
+
warning_threshold_reached=warning,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def get_sprint_metrics(
|
|
162
|
+
db: AsyncSession, goal_id: str | None = None
|
|
163
|
+
) -> SprintMetrics:
|
|
164
|
+
"""Calculate sprint progress metrics."""
|
|
165
|
+
base_query = select(Ticket)
|
|
166
|
+
if goal_id:
|
|
167
|
+
base_query = base_query.where(Ticket.goal_id == goal_id)
|
|
168
|
+
|
|
169
|
+
result = await db.execute(base_query)
|
|
170
|
+
tickets = result.scalars().all()
|
|
171
|
+
|
|
172
|
+
if not tickets:
|
|
173
|
+
return SprintMetrics()
|
|
174
|
+
|
|
175
|
+
total = len(tickets)
|
|
176
|
+
completed = sum(1 for t in tickets if t.state == TicketState.DONE.value)
|
|
177
|
+
in_progress = sum(
|
|
178
|
+
1
|
|
179
|
+
for t in tickets
|
|
180
|
+
if t.state
|
|
181
|
+
in [
|
|
182
|
+
TicketState.EXECUTING.value,
|
|
183
|
+
TicketState.VERIFYING.value,
|
|
184
|
+
TicketState.NEEDS_HUMAN.value,
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
blocked = sum(1 for t in tickets if t.state == TicketState.BLOCKED.value)
|
|
188
|
+
|
|
189
|
+
# Calculate velocity (tickets completed in last 7 days)
|
|
190
|
+
week_ago = datetime.utcnow() - timedelta(days=7)
|
|
191
|
+
completed_recently = sum(
|
|
192
|
+
1
|
|
193
|
+
for t in tickets
|
|
194
|
+
if t.state == TicketState.DONE.value and t.updated_at >= week_ago
|
|
195
|
+
)
|
|
196
|
+
velocity = completed_recently / 7.0
|
|
197
|
+
|
|
198
|
+
# Average cycle time (from created to done)
|
|
199
|
+
done_tickets = [t for t in tickets if t.state == TicketState.DONE.value]
|
|
200
|
+
if done_tickets:
|
|
201
|
+
cycle_times = [
|
|
202
|
+
(t.updated_at - t.created_at).total_seconds() / 3600 for t in done_tickets
|
|
203
|
+
]
|
|
204
|
+
avg_cycle = sum(cycle_times) / len(cycle_times)
|
|
205
|
+
else:
|
|
206
|
+
avg_cycle = 0
|
|
207
|
+
|
|
208
|
+
return SprintMetrics(
|
|
209
|
+
total_tickets=total,
|
|
210
|
+
completed_tickets=completed,
|
|
211
|
+
in_progress_tickets=in_progress,
|
|
212
|
+
blocked_tickets=blocked,
|
|
213
|
+
completion_rate=round((completed / total) * 100, 1) if total > 0 else 0,
|
|
214
|
+
avg_cycle_time_hours=round(avg_cycle, 1),
|
|
215
|
+
velocity=round(velocity, 1),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def get_agent_metrics(
|
|
220
|
+
db: AsyncSession, goal_id: str | None = None
|
|
221
|
+
) -> AgentMetrics:
|
|
222
|
+
"""Calculate AI agent usage metrics using SQL aggregation."""
|
|
223
|
+
# Build base filter condition
|
|
224
|
+
filters = []
|
|
225
|
+
if goal_id:
|
|
226
|
+
filters.append(Ticket.goal_id == goal_id)
|
|
227
|
+
|
|
228
|
+
# Aggregate totals in a single query
|
|
229
|
+
totals_query = select(
|
|
230
|
+
func.count(AgentSession.id).label("total"),
|
|
231
|
+
func.count(AgentSession.ended_at)
|
|
232
|
+
.filter(AgentSession.turn_count > 0)
|
|
233
|
+
.label("successful"),
|
|
234
|
+
func.coalesce(func.avg(AgentSession.turn_count), 0).label("avg_turns"),
|
|
235
|
+
func.coalesce(func.sum(AgentSession.estimated_cost_usd), 0).label("total_cost"),
|
|
236
|
+
)
|
|
237
|
+
if goal_id:
|
|
238
|
+
totals_query = totals_query.join(Ticket).where(Ticket.goal_id == goal_id)
|
|
239
|
+
|
|
240
|
+
totals_result = await db.execute(totals_query)
|
|
241
|
+
row = totals_result.one()
|
|
242
|
+
total = row.total
|
|
243
|
+
successful = row.successful
|
|
244
|
+
avg_turns = float(row.avg_turns)
|
|
245
|
+
total_cost = float(row.total_cost)
|
|
246
|
+
|
|
247
|
+
if total == 0:
|
|
248
|
+
return AgentMetrics()
|
|
249
|
+
|
|
250
|
+
# Most used agent via GROUP BY
|
|
251
|
+
agent_query = select(
|
|
252
|
+
AgentSession.agent_type, func.count(AgentSession.id).label("cnt")
|
|
253
|
+
).group_by(AgentSession.agent_type)
|
|
254
|
+
if goal_id:
|
|
255
|
+
agent_query = agent_query.join(Ticket).where(Ticket.goal_id == goal_id)
|
|
256
|
+
agent_query = agent_query.order_by(func.count(AgentSession.id).desc()).limit(1)
|
|
257
|
+
|
|
258
|
+
agent_result = await db.execute(agent_query)
|
|
259
|
+
agent_row = agent_result.first()
|
|
260
|
+
most_used = agent_row[0] if agent_row else "claude"
|
|
261
|
+
|
|
262
|
+
return AgentMetrics(
|
|
263
|
+
total_sessions=total,
|
|
264
|
+
successful_sessions=successful,
|
|
265
|
+
success_rate=round((successful / total) * 100, 1) if total > 0 else 0,
|
|
266
|
+
avg_turns_per_session=round(avg_turns, 1),
|
|
267
|
+
most_used_agent=most_used,
|
|
268
|
+
total_cost_usd=round(total_cost, 2),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
async def get_cost_trend(
|
|
273
|
+
db: AsyncSession, days: int = 7, goal_id: str | None = None
|
|
274
|
+
) -> list[CostTrendItem]:
|
|
275
|
+
"""Get daily cost trend for the last N days."""
|
|
276
|
+
trends = []
|
|
277
|
+
now = datetime.utcnow()
|
|
278
|
+
|
|
279
|
+
for i in range(days - 1, -1, -1):
|
|
280
|
+
day = now - timedelta(days=i)
|
|
281
|
+
day_start = day.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
282
|
+
day_end = day_start + timedelta(days=1)
|
|
283
|
+
|
|
284
|
+
cost = await get_period_cost(db, day_start, day_end, goal_id)
|
|
285
|
+
|
|
286
|
+
trends.append(
|
|
287
|
+
CostTrendItem(
|
|
288
|
+
date=day_start.strftime("%a"), # Mon, Tue, etc.
|
|
289
|
+
cost=round(cost, 2),
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return trends
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ============================================================================
|
|
297
|
+
# API Endpoints
|
|
298
|
+
# ============================================================================
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@router.get("", response_model=DashboardResponse)
|
|
302
|
+
async def get_dashboard(
|
|
303
|
+
goal_id: str | None = Query(None, description="Filter by goal ID"),
|
|
304
|
+
daily_budget: float = Query(10.0, description="Daily budget limit"),
|
|
305
|
+
weekly_budget: float = Query(50.0, description="Weekly budget limit"),
|
|
306
|
+
monthly_budget: float = Query(150.0, description="Monthly budget limit"),
|
|
307
|
+
db: AsyncSession = Depends(get_db),
|
|
308
|
+
) -> DashboardResponse:
|
|
309
|
+
"""Get complete dashboard data with metrics and budget status."""
|
|
310
|
+
budget = await get_budget_status(
|
|
311
|
+
db, goal_id, daily_budget, weekly_budget, monthly_budget
|
|
312
|
+
)
|
|
313
|
+
sprint = await get_sprint_metrics(db, goal_id)
|
|
314
|
+
agent = await get_agent_metrics(db, goal_id)
|
|
315
|
+
cost_trend = await get_cost_trend(db, 7, goal_id)
|
|
316
|
+
|
|
317
|
+
return DashboardResponse(
|
|
318
|
+
budget=budget, sprint=sprint, agent=agent, cost_trend=cost_trend
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@router.get("/budget", response_model=BudgetStatus)
|
|
323
|
+
async def get_budget(
|
|
324
|
+
goal_id: str | None = Query(None, description="Filter by goal ID"),
|
|
325
|
+
daily_budget: float = Query(10.0),
|
|
326
|
+
weekly_budget: float = Query(50.0),
|
|
327
|
+
monthly_budget: float = Query(150.0),
|
|
328
|
+
db: AsyncSession = Depends(get_db),
|
|
329
|
+
) -> BudgetStatus:
|
|
330
|
+
"""Get current budget status."""
|
|
331
|
+
return await get_budget_status(
|
|
332
|
+
db, goal_id, daily_budget, weekly_budget, monthly_budget
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@router.get("/sprint", response_model=SprintMetrics)
|
|
337
|
+
async def get_sprint(
|
|
338
|
+
goal_id: str | None = Query(None, description="Filter by goal ID"),
|
|
339
|
+
db: AsyncSession = Depends(get_db),
|
|
340
|
+
) -> SprintMetrics:
|
|
341
|
+
"""Get sprint progress metrics."""
|
|
342
|
+
return await get_sprint_metrics(db, goal_id)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@router.get("/agent-metrics", response_model=AgentMetrics)
|
|
346
|
+
async def get_agents(
|
|
347
|
+
goal_id: str | None = Query(None, description="Filter by goal ID"),
|
|
348
|
+
db: AsyncSession = Depends(get_db),
|
|
349
|
+
) -> AgentMetrics:
|
|
350
|
+
"""Get AI agent usage metrics."""
|
|
351
|
+
return await get_agent_metrics(db, goal_id)
|