fix: resolve four frontend/backend issues

- Fix ProjectsPage tests by wrapping renders in MemoryRouter (9 passing)
- Improve session auto-naming to 'project / repo / tool' format
- Add missing /users/me/sessions endpoint for sidebar session loading
- Handle git history 500s: catch RuntimeError in endpoints, graceful empty repo handling
- Add git status badge and discard-changes button to FileEditor toolbar

Quality gates: tsc pass, build pass, Python syntax pass
This commit is contained in:
Developer
2026-06-03 08:30:28 +00:00
parent 543fee5d56
commit 5ed5e1c84b
9 changed files with 348 additions and 158 deletions
+17 -3
View File
@@ -3,7 +3,7 @@
import logging
import uuid
from fastapi import APIRouter, Depends, Response, status
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user, get_db_session, get_owned_project
@@ -94,7 +94,14 @@ async def get_repository_history(
from src.services.git.repository import get_repo_and_validate, ensure_repo_on_disk
repo = await get_repo_and_validate(session, repo_id, project_id)
ensure_repo_on_disk(repo)
return get_commit_history(repo.path, branch=branch, limit=limit, offset=offset)
try:
return get_commit_history(repo.path, branch=branch, limit=limit, offset=offset)
except RuntimeError as e:
logger.warning("Git history failed for %s: %s", repo.path, str(e))
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Git history unavailable: {str(e)}",
) from e
@router.get("/{project_id}/repositories/{repo_id}/commits/{commit_hash}")
@@ -110,7 +117,14 @@ async def get_repository_commit(
from src.services.git.repository import get_repo_and_validate, ensure_repo_on_disk
repo = await get_repo_and_validate(session, repo_id, project_id)
ensure_repo_on_disk(repo)
return get_commit_detail(repo.path, commit_hash)
try:
return get_commit_detail(repo.path, commit_hash)
except RuntimeError as e:
logger.warning("Git commit detail failed for %s %s: %s", repo.path, commit_hash, str(e))
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Commit detail unavailable: {str(e)}",
) from e
# File browsing
+41
View File
@@ -2,10 +2,13 @@ import uuid
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user, get_db_session
from src.models.tool_instance import ToolInstance
from src.models.user import User
from src.schemas.tool_instance import SessionItemResponse, SessionListResponse
from src.schemas.user import UserProfileResponse, UserProfileUpdate
router = APIRouter(prefix="/users", tags=["users"])
@@ -131,3 +134,41 @@ async def upload_avatar(
await session.commit()
await session.refresh(user)
return user
@router.get(
"/me/sessions",
response_model=SessionListResponse,
summary="Get current user sessions",
description="Retrieve all tool instances (sessions) for the authenticated user.",
)
async def get_user_sessions(
user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_db_session),
) -> SessionListResponse:
"""Return all tool instances for the current user with related names."""
result = await session.execute(
select(ToolInstance)
.where(ToolInstance.owner_id == user.id)
.order_by(ToolInstance.created_at.desc())
)
instances = result.scalars().all()
sessions = [
SessionItemResponse(
id=str(inst.id),
display_name=inst.display_name,
tool_type_name=inst.tool_type.display_name if inst.tool_type else "Unknown",
tool_icon=inst.tool_type.icon if inst.tool_type else None,
tool_type_interfaces=inst.tool_type.interfaces if inst.tool_type else [],
repository_name=inst.repository.name if inst.repository else "Unknown",
repository_id=str(inst.repository_id),
project_name=inst.project.name if inst.project else "Unknown",
project_id=str(inst.project_id),
status=inst.status,
url=inst.url,
)
for inst in instances
]
return SessionListResponse(sessions=sessions)
+24
View File
@@ -15,3 +15,27 @@ class CreateInstanceRequest(BaseModel):
config_profile_id: str | None = Field(
default=None, description="Optional config profile ID to apply to the instance"
)
class SessionItemResponse(BaseModel):
"""Lightweight session summary for sidebar and dashboard."""
model_config = {"extra": "ignore"}
id: str = Field(description="Session (tool instance) ID")
display_name: str = Field(description="Display name of the session")
tool_type_name: str = Field(description="Name of the tool type")
tool_icon: str | None = Field(default=None, description="Icon URL for the tool type")
tool_type_interfaces: list[str] = Field(default_factory=list, description="Supported interfaces")
repository_name: str = Field(description="Name of the repository")
repository_id: str = Field(description="Repository ID")
project_name: str = Field(description="Name of the project")
project_id: str = Field(description="Project ID")
status: str = Field(description="Current status")
url: str | None = Field(default=None, description="Access URL")
class SessionListResponse(BaseModel):
"""Response wrapping a list of session summaries."""
sessions: list[SessionItemResponse]
+1 -1
View File
@@ -54,7 +54,7 @@ async def create_new_instance(
instance = ToolInstance(
name=instance_name,
display_name=display_name or f"{tool_type.display_name} - {repo.name}",
display_name=display_name or f"{project.name} / {repo.name} / {tool_type.display_name}",
tool_type_id=tool_type.id,
repository_id=repo.id,
project_id=project.id,
+21 -6
View File
@@ -60,9 +60,12 @@ def get_commit_history(repo_path: str, branch: str | None = None, limit: int = 1
Returns structured data including commits, branches, and graph information.
"""
# Get list of branches
branches_output = _run_git_command(repo_path, ["branch", "-a", "--format=%(refname:short)"])
branches = [b.strip() for b in branches_output.strip().split("\n") if b.strip()]
# Get list of branches (may fail for empty repos)
try:
branches_output = _run_git_command(repo_path, ["branch", "-a", "--format=%(refname:short)"])
branches = [b.strip() for b in branches_output.strip().split("\n") if b.strip()]
except RuntimeError:
branches = []
# Build git log command - use NULL bytes as separators to avoid parsing issues
log_args = [
@@ -76,7 +79,16 @@ def get_commit_history(repo_path: str, branch: str | None = None, limit: int = 1
else:
log_args.append("--all")
log_output = _run_git_command(repo_path, log_args)
try:
log_output = _run_git_command(repo_path, log_args)
except RuntimeError:
# Empty repo or no commits
return {
"commits": [],
"branches": branches,
"total_commits": 0,
"graph_data": {"nodes": [], "edges": []},
}
# Get branch info for each commit
branch_map = _get_branch_map(repo_path)
@@ -113,8 +125,11 @@ def get_commit_history(repo_path: str, branch: str | None = None, limit: int = 1
)
# Get total commit count
count_output = _run_git_command(repo_path, ["rev-list", "--all", "--count"])
total_commits = int(count_output.strip()) if count_output.strip() else 0
try:
count_output = _run_git_command(repo_path, ["rev-list", "--all", "--count"])
total_commits = int(count_output.strip()) if count_output.strip() else 0
except RuntimeError:
total_commits = 0
# Build graph data and generate graph symbols
graph_data = _build_graph_data(commits)