cc-dashboard/src/routers/projects.py
Vadym Samoilenko 26127061ec fix: OMG auto-sync, Projects OMG# column, ADO OMG Deliverable Number, session persistence
- Auto-create/update OmgEntry when Project.job_number changes (PATCH /api/projects);
  delete stale entry on clear; sync name/client when those fields change too
- Backfill script: scripts/backfill_omg_from_projects.py
- Projects List-view: add OMG # column with link to /omg?highlight=<job_number>;
  Grid-view badge also made clickable; OmgView supports ?highlight= deep-link with scroll+highlight
- AzureWorkItem: add omg_number column (migration 0009), extracted from
  fields_json[Custom.OMGDeliverableNumber] on sync; DevOps table shows OMG # column
  with CC-project link when matched; toolbar badge shows count of items without OMG #
- Session no longer lost on F5: refresh_token moved to HttpOnly+SameSite=Lax cookie;
  authStore.init() restores session on app start; axios interceptor retries on 401
  via cookie refresh before logging out; POST /api/auth/logout clears cookie

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 12:30:40 +01:00

99 lines
3.1 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.auth import CurrentUser
from src.database import get_db
from src.models import OmgEntry, Project
from src.schemas import ProjectOut
router = APIRouter(prefix="/api/projects", tags=["projects"])
@router.get("", response_model=list[ProjectOut])
async def list_projects(user: CurrentUser, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Project).where(Project.user_id == user.id).order_by(Project.display_name)
)
return result.scalars().all()
@router.patch("/{project_id}", response_model=ProjectOut)
async def update_project(
project_id: str,
body: dict,
user: CurrentUser,
db: AsyncSession = Depends(get_db),
):
project = await db.get(Project, project_id)
if not project or project.user_id != user.id:
raise HTTPException(status_code=404, detail="Project not found")
old_job_number = project.job_number
if "display_name" in body:
project.display_name = str(body["display_name"])[:255]
if "client" in body:
project.client = str(body["client"])[:255]
if "job_number" in body:
project.job_number = str(body["job_number"])[:100]
if "repo_url" in body:
project.repo_url = str(body["repo_url"])[:500]
await _sync_omg_entry(user.id, project, old_job_number, body, db)
await db.commit()
await db.refresh(project)
return project
async def _sync_omg_entry(
user_id: str,
project: Project,
old_job_number: str,
changed_fields: dict,
db: AsyncSession,
) -> None:
"""Keep omg_entries in sync when project job_number / name / client changes."""
job_changed = "job_number" in changed_fields
meta_changed = "display_name" in changed_fields or "client" in changed_fields
if job_changed and old_job_number != project.job_number:
# Remove stale entry for the old number
if old_job_number:
res = await db.execute(
select(OmgEntry).where(
OmgEntry.user_id == user_id,
OmgEntry.job_number == old_job_number,
)
)
old_entry = res.scalar_one_or_none()
if old_entry:
await db.delete(old_entry)
if project.job_number:
await _upsert_omg(user_id, project, db)
elif meta_changed and project.job_number:
# Name or client changed but job_number stayed — update existing entry if any
await _upsert_omg(user_id, project, db)
async def _upsert_omg(user_id: str, project: Project, db: AsyncSession) -> None:
res = await db.execute(
select(OmgEntry).where(
OmgEntry.user_id == user_id,
OmgEntry.job_number == project.job_number,
)
)
entry = res.scalar_one_or_none()
if entry:
entry.name = project.display_name
entry.client = project.client
else:
db.add(OmgEntry(
user_id=user_id,
name=project.display_name,
client=project.client,
job_number=project.job_number,
))