diff --git a/mcp-server/src/legal_mcp/tools/cases.py b/mcp-server/src/legal_mcp/tools/cases.py index 4ce03db..7c07717 100644 --- a/mcp-server/src/legal_mcp/tools/cases.py +++ b/mcp-server/src/legal_mcp/tools/cases.py @@ -28,12 +28,17 @@ def _gitea_token() -> str: return os.environ.get("GITEA_ACCESS_TOKEN") or os.environ.get("GITEA_TOKEN", "") -async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> bool: - """Create Gitea repo and configure git remote. Best-effort — returns False on failure.""" +async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> dict: + """Create Gitea repo and configure git remote. + + Returns a dict with: ok (bool), url (str|None), error (str|None). + Never raises — failures are reported via the dict so callers can surface + them to the UI instead of silently swallowing them. + """ token = _gitea_token() if not token: logger.info("No GITEA_TOKEN — skipping Gitea repo creation for %s", case_number) - return False + return {"ok": False, "url": None, "error": "no_token"} try: async with httpx.AsyncClient(verify=False, timeout=30) as client: @@ -59,8 +64,9 @@ async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> b repo = resp.json() clone_url = repo.get("clone_url", "") + html_url = repo.get("html_url", "") if not clone_url: - return False + return {"ok": False, "url": None, "error": "no_clone_url"} auth_url = clone_url.replace("https://", f"https://chaim:{token}@") @@ -94,15 +100,20 @@ async def _setup_gitea_remote(case_number: str, title: str, case_dir: Path) -> b cwd=case_dir, capture_output=True, text=True, env=git_env, ) if push.returncode != 0: - logger.warning("Gitea push failed for %s: %s", case_number, push.stderr) - return False + stderr = push.stderr.strip() + logger.warning("Gitea push failed for %s: %s", case_number, stderr) + return {"ok": False, "url": html_url or None, "error": f"push_failed: {stderr[:200]}"} logger.info("Gitea repo created and pushed for %s", case_number) - return True + return {"ok": True, "url": html_url or None, "error": None} + except httpx.HTTPStatusError as exc: + msg = f"http_{exc.response.status_code}" + logger.warning("Gitea setup failed for %s: %s", case_number, msg) + return {"ok": False, "url": None, "error": msg} except Exception as exc: logger.warning("Gitea setup failed for %s: %s", case_number, exc) - return False + return {"ok": False, "url": None, "error": f"{type(exc).__name__}: {exc}"[:200]} async def case_create( @@ -214,11 +225,10 @@ async def case_create( except Exception: pass # git not available — non-critical - # Create Gitea repo and configure remote (best-effort) - try: - await _setup_gitea_remote(case_number, title, case_dir) - except Exception: - pass # Gitea not available — non-critical + # Create Gitea repo and configure remote — surface result so callers can + # show failures (e.g. stale token) and offer a retry button instead of + # silently producing a case with no remote. + case["gitea"] = await _setup_gitea_remote(case_number, title, case_dir) return json.dumps(case, default=str, ensure_ascii=False, indent=2) diff --git a/web-ui/src/components/cases/case-header.tsx b/web-ui/src/components/cases/case-header.tsx index e2b70d4..c4c48ae 100644 --- a/web-ui/src/components/cases/case-header.tsx +++ b/web-ui/src/components/cases/case-header.tsx @@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge"; import { StatusBadge } from "@/components/cases/status-badge"; import { SyncIndicator } from "@/components/cases/sync-indicator"; import { CaseArchiveAction } from "@/components/cases/case-archive-action"; +import { CreateRepoButton } from "@/components/cases/create-repo-button"; import { PRACTICE_AREA_LABELS, APPEAL_SUBTYPE_LABELS, @@ -67,6 +68,7 @@ export function CaseHeader({ data }: { data?: CaseDetail }) { archivedAt={data.archived_at} /> )} +

{data?.title ?? "טוען…"} diff --git a/web-ui/src/components/cases/create-repo-button.tsx b/web-ui/src/components/cases/create-repo-button.tsx new file mode 100644 index 0000000..17cf124 --- /dev/null +++ b/web-ui/src/components/cases/create-repo-button.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { toast } from "sonner"; +import { Cloud, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + useCreateGiteaRepo, + useGitStatus, + type CaseDetail, +} from "@/lib/api/cases"; + +export function CreateRepoButton({ data }: { data?: CaseDetail }) { + const { data: gitStatus } = useGitStatus(data?.case_number); + const createRepo = useCreateGiteaRepo(data?.case_number); + + if (!data?.case_number || !gitStatus) return null; + + /* Show only when something is actually missing — repo, remote, or + * unpushed commits. If everything is in sync, the SyncIndicator already + * communicates that. */ + const needsRepo = gitStatus.error === "no_repo" || !gitStatus.has_remote; + if (!needsRepo) return null; + + function handleClick() { + if (!data) return; + createRepo.mutate( + { title: data.title ?? "", description: data.subject ?? "" }, + { + onSuccess: (res) => + toast.success( + res.pushed + ? `הריפו נוצר ב-Gitea: ${res.repo_url}` + : `הריפו נוצר אך ה-push נכשל. בדוק את הלוגים של ה-backend.`, + ), + onError: (err) => + toast.error( + err instanceof Error ? err.message : "יצירת הריפו נכשלה", + ), + }, + ); + } + + return ( + + ); +} diff --git a/web-ui/src/lib/api/cases.ts b/web-ui/src/lib/api/cases.ts index 6018342..59aa637 100644 --- a/web-ui/src/lib/api/cases.ts +++ b/web-ui/src/lib/api/cases.ts @@ -217,6 +217,30 @@ export function useGitStatus(caseNumber: string | undefined) { }); } +export type CreateGiteaRepoResult = { + repo_url: string; + clone_url: string; + pushed: boolean; +}; + +export function useCreateGiteaRepo(caseNumber: string | undefined) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (input: { title: string; description?: string }) => + apiRequest(`/api/integrations/gitea/create-repo`, { + method: "POST", + body: { + case_number: caseNumber, + title: input.title, + description: input.description ?? "", + }, + }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: [...casesKeys.all, "git-status", caseNumber ?? ""] }); + }, + }); +} + export type StartWorkflowResult = { case_number: string; status: string; diff --git a/web/app.py b/web/app.py index dcb3561..2cc83bf 100644 --- a/web/app.py +++ b/web/app.py @@ -1214,6 +1214,15 @@ async def api_case_create(req: CaseCreateRequest): logger.warning("Failed to auto-create Paperclip project for case %s: %s", req.case_number, e) parsed["paperclip_error"] = str(e) + # Gitea result was attached by case_create itself; log if it failed so + # ops can spot stale-token issues without scanning every response. + gitea_result = parsed.get("gitea") or {} + if not gitea_result.get("ok"): + logger.warning( + "Gitea repo not created for case %s: %s", + req.case_number, gitea_result.get("error") or "unknown", + ) + return parsed