feat: implement full arch design dashboard #1
|
|
@ -15,6 +15,8 @@ from app.modules.graph.application.services import GraphService
|
|||
from app.modules.graph.interfaces.http.router import router as graph_router, init_router as init_graph_router
|
||||
from app.modules.editor.application.services import EditorService
|
||||
from app.modules.editor.interfaces.http.router import router as editor_router, init_router as init_editor_router
|
||||
from app.modules.impl_tracker.application.services import ImplTrackerService
|
||||
from app.modules.impl_tracker.interfaces.http.router import router as impl_tracker_router, init_router as init_impl_tracker_router
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
|
|
@ -40,11 +42,16 @@ def create_app() -> FastAPI:
|
|||
editor_service = EditorService(scan_service)
|
||||
init_editor_router(project_service, scan_service, editor_service)
|
||||
|
||||
# Wire Impl-tracker module
|
||||
impl_tracker_service = ImplTrackerService()
|
||||
init_impl_tracker_router(project_service, scan_service, impl_tracker_service)
|
||||
|
||||
# Register routers
|
||||
app.include_router(project_router, prefix="/api")
|
||||
app.include_router(scanner_router, prefix="/api")
|
||||
app.include_router(graph_router, prefix="/api")
|
||||
app.include_router(editor_router, prefix="/api")
|
||||
app.include_router(impl_tracker_router, prefix="/api")
|
||||
|
||||
# Health check
|
||||
@app.get("/api/health")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
from datetime import datetime, timezone
|
||||
|
||||
from app.modules.impl_tracker.domain.entities import ImplProgress
|
||||
from app.modules.impl_tracker.infrastructure.code_scanner import scan_code_directory
|
||||
from app.modules.project.domain.entities import Project
|
||||
from app.modules.scanner.domain.entities import ScanResult
|
||||
|
||||
|
||||
class ImplTrackerService:
|
||||
def __init__(self) -> None:
|
||||
self._cache: dict[str, list[ImplProgress]] = {}
|
||||
self._manual_overrides: dict[str, dict[str, float]] = {} # project_id -> {module_id: percentage}
|
||||
|
||||
def evaluate(self, project: Project, scan_result: ScanResult) -> list[ImplProgress]:
|
||||
progress_list: list[ImplProgress] = []
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
if not project.code_dir:
|
||||
# No code dir -> all modules at 0%
|
||||
for mod in scan_result.modules:
|
||||
progress_list.append(ImplProgress(
|
||||
module_id=mod.module_id, percentage=0.0, source="auto", evaluated_at=now,
|
||||
))
|
||||
else:
|
||||
code_structure = scan_code_directory(project.code_dir, scan_result)
|
||||
for mod in scan_result.modules:
|
||||
if mod.module_id in code_structure.matched_modules:
|
||||
percentage = 50.0 # Basic: module directory exists
|
||||
else:
|
||||
percentage = 0.0
|
||||
progress_list.append(ImplProgress(
|
||||
module_id=mod.module_id, percentage=percentage, source="auto", evaluated_at=now,
|
||||
))
|
||||
|
||||
# Apply manual overrides
|
||||
overrides = self._manual_overrides.get(project.id, {})
|
||||
for p in progress_list:
|
||||
if p.module_id in overrides:
|
||||
p.percentage = overrides[p.module_id]
|
||||
p.source = "manual"
|
||||
|
||||
self._cache[project.id] = progress_list
|
||||
return progress_list
|
||||
|
||||
def get_progress(self, project_id: str) -> list[ImplProgress] | None:
|
||||
return self._cache.get(project_id)
|
||||
|
||||
def set_manual_progress(self, project_id: str, module_id: str, percentage: float) -> None:
|
||||
if project_id not in self._manual_overrides:
|
||||
self._manual_overrides[project_id] = {}
|
||||
self._manual_overrides[project_id][module_id] = percentage
|
||||
# Update cache if exists
|
||||
if project_id in self._cache:
|
||||
for p in self._cache[project_id]:
|
||||
if p.module_id == module_id:
|
||||
p.percentage = percentage
|
||||
p.source = "manual"
|
||||
p.evaluated_at = datetime.now(timezone.utc)
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
"""Impl-tracker HTTP router — progress evaluation and manual override endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.modules.impl_tracker.application.services import ImplTrackerService
|
||||
from app.modules.impl_tracker.domain.entities import ImplProgress
|
||||
from app.modules.project.application.services import ProjectService
|
||||
from app.modules.scanner.application.services import ScanService
|
||||
|
||||
router = APIRouter(prefix="/projects/{project_id}/impl-progress", tags=["impl-tracker"])
|
||||
|
||||
_project_service: ProjectService | None = None
|
||||
_scan_service: ScanService | None = None
|
||||
_impl_tracker_service: ImplTrackerService | None = None
|
||||
|
||||
|
||||
def init_router(
|
||||
project_service: ProjectService,
|
||||
scan_service: ScanService,
|
||||
impl_tracker_service: ImplTrackerService,
|
||||
) -> None:
|
||||
global _project_service, _scan_service, _impl_tracker_service
|
||||
_project_service = project_service
|
||||
_scan_service = scan_service
|
||||
_impl_tracker_service = impl_tracker_service
|
||||
|
||||
|
||||
class ManualProgressRequest(BaseModel):
|
||||
percentage: float
|
||||
|
||||
|
||||
def _progress_to_dict(p) -> dict:
|
||||
d = asdict(p)
|
||||
d["evaluated_at"] = p.evaluated_at.isoformat()
|
||||
return d
|
||||
|
||||
|
||||
@router.post("")
|
||||
def evaluate_progress(project_id: str):
|
||||
"""Evaluate implementation progress for all modules."""
|
||||
project = _project_service.get_project(project_id)
|
||||
scan_result = _scan_service.get_latest_scan(project_id)
|
||||
if scan_result is None:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"detail": "No scan available. Run POST /scan first."},
|
||||
)
|
||||
progress = _impl_tracker_service.evaluate(project, scan_result)
|
||||
return [_progress_to_dict(p) for p in progress]
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_progress(project_id: str):
|
||||
"""Get cached implementation progress."""
|
||||
_project_service.get_project(project_id) # Ensure project exists (raises 404)
|
||||
progress = _impl_tracker_service.get_progress(project_id)
|
||||
if progress is None:
|
||||
return JSONResponse(
|
||||
status_code=404,
|
||||
content={"detail": "No progress evaluated yet. Run POST /impl-progress first."},
|
||||
)
|
||||
return [_progress_to_dict(p) for p in progress]
|
||||
|
||||
|
||||
@router.put("/{module_id}")
|
||||
def set_manual_progress(project_id: str, module_id: str, body: ManualProgressRequest):
|
||||
"""Set manual progress override for a module."""
|
||||
_project_service.get_project(project_id) # Ensure project exists (raises 404)
|
||||
_impl_tracker_service.set_manual_progress(project_id, module_id, body.percentage)
|
||||
|
||||
# Return the updated progress entry
|
||||
progress = _impl_tracker_service.get_progress(project_id)
|
||||
if progress:
|
||||
for p in progress:
|
||||
if p.module_id == module_id:
|
||||
return _progress_to_dict(p)
|
||||
|
||||
# If no cached progress, return a constructed response
|
||||
return _progress_to_dict(
|
||||
ImplProgress(
|
||||
module_id=module_id,
|
||||
percentage=body.percentage,
|
||||
source="manual",
|
||||
evaluated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
46
backend/tests/test_api_impl_tracker.py
Normal file
46
backend/tests/test_api_impl_tracker.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import pytest
|
||||
|
||||
DESIGN_DIR = "/workspace/arch-design-agent-skill-dashboard/design"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project_id(client):
|
||||
r = client.post("/api/projects", json={"name": "test", "design_dir": DESIGN_DIR})
|
||||
return r.json()["id"]
|
||||
|
||||
|
||||
def test_evaluate_progress(client, project_id):
|
||||
# Need to scan first
|
||||
client.post(f"/api/projects/{project_id}/scan")
|
||||
r = client.post(f"/api/projects/{project_id}/impl-progress")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) > 0
|
||||
assert "module_id" in data[0]
|
||||
assert "percentage" in data[0]
|
||||
|
||||
|
||||
def test_get_progress_not_evaluated(client, project_id):
|
||||
r = client.get(f"/api/projects/{project_id}/impl-progress")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_get_progress_after_evaluate(client, project_id):
|
||||
client.post(f"/api/projects/{project_id}/scan")
|
||||
client.post(f"/api/projects/{project_id}/impl-progress")
|
||||
r = client.get(f"/api/projects/{project_id}/impl-progress")
|
||||
assert r.status_code == 200
|
||||
assert isinstance(r.json(), list)
|
||||
|
||||
|
||||
def test_set_manual_progress(client, project_id):
|
||||
client.post(f"/api/projects/{project_id}/scan")
|
||||
client.post(f"/api/projects/{project_id}/impl-progress")
|
||||
r = client.put(
|
||||
f"/api/projects/{project_id}/impl-progress/MOD-PROJECT",
|
||||
json={"percentage": 80.0},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["percentage"] == 80.0
|
||||
assert r.json()["source"] == "manual"
|
||||
59
backend/tests/test_impl_tracker.py
Normal file
59
backend/tests/test_impl_tracker.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import pytest
|
||||
from datetime import datetime
|
||||
|
||||
from app.modules.impl_tracker.application.services import ImplTrackerService
|
||||
from app.modules.project.domain.entities import Project
|
||||
from app.modules.scanner.application.services import ScanService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def impl_service():
|
||||
return ImplTrackerService()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scan_result():
|
||||
svc = ScanService()
|
||||
project = Project(
|
||||
id="test", name="test",
|
||||
design_dir="/workspace/arch-design-agent-skill-dashboard/design",
|
||||
code_dir=None, created_at=datetime(2026, 1, 1),
|
||||
)
|
||||
return svc.scan(project)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_project():
|
||||
return Project(
|
||||
id="test", name="test",
|
||||
design_dir="/workspace/arch-design-agent-skill-dashboard/design",
|
||||
code_dir=None, created_at=datetime(2026, 1, 1),
|
||||
)
|
||||
|
||||
|
||||
def test_evaluate_no_code_dir(impl_service, test_project, scan_result):
|
||||
progress = impl_service.evaluate(test_project, scan_result)
|
||||
assert len(progress) > 0
|
||||
assert all(p.percentage == 0.0 for p in progress)
|
||||
assert all(p.source == "auto" for p in progress)
|
||||
|
||||
|
||||
def test_get_progress_before_evaluate(impl_service):
|
||||
assert impl_service.get_progress("nonexistent") is None
|
||||
|
||||
|
||||
def test_get_progress_after_evaluate(impl_service, test_project, scan_result):
|
||||
impl_service.evaluate(test_project, scan_result)
|
||||
cached = impl_service.get_progress("test")
|
||||
assert cached is not None
|
||||
assert len(cached) > 0
|
||||
|
||||
|
||||
def test_set_manual_progress(impl_service, test_project, scan_result):
|
||||
impl_service.evaluate(test_project, scan_result)
|
||||
impl_service.set_manual_progress("test", "MOD-PROJECT", 75.0)
|
||||
cached = impl_service.get_progress("test")
|
||||
mod_progress = [p for p in cached if p.module_id == "MOD-PROJECT"]
|
||||
assert len(mod_progress) == 1
|
||||
assert mod_progress[0].percentage == 75.0
|
||||
assert mod_progress[0].source == "manual"
|
||||
Loading…
Reference in New Issue
Block a user