diff --git a/web/app.py b/web/app.py
index 80bb096..51200dc 100644
--- a/web/app.py
+++ b/web/app.py
@@ -17,7 +17,7 @@ from uuid import UUID, uuid4
# Allow importing legal_mcp from the MCP server source
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "mcp-server" / "src"))
-from fastapi import FastAPI, File, HTTPException, UploadFile
+from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
@@ -26,6 +26,12 @@ from legal_mcp import config
from legal_mcp.services import chunker, db, embeddings, extractor, processor
from legal_mcp.tools import cases as cases_tools, search as search_tools, workflow as workflow_tools, drafting as drafting_tools
+# Import integration clients (same directory)
+_web_dir = Path(__file__).resolve().parent
+sys.path.insert(0, str(_web_dir.parent))
+from web.gitea_client import create_repo, setup_remote_and_push
+from web.paperclip_client import create_project as pc_create_project, get_project_url
+
logger = logging.getLogger(__name__)
@@ -178,17 +184,34 @@ async def health():
@app.get("/api/cases")
-async def list_cases():
- """List existing cases for the dropdown."""
+async def list_cases(detail: bool = False):
+ """List existing cases. With detail=true, includes doc counts and integration URLs."""
cases = await db.list_cases()
- return [
- {
- "case_number": c["case_number"],
- "title": c["title"],
- "status": c["status"],
- }
- for c in cases
- ]
+ if not detail:
+ return [
+ {"case_number": c["case_number"], "title": c["title"], "status": c["status"]}
+ for c in cases
+ ]
+ # Enhanced listing with document counts
+ pool = await db.get_pool()
+ result = []
+ async with pool.acquire() as conn:
+ for c in cases:
+ case_id = UUID(c["id"])
+ doc_count = await conn.fetchval(
+ "SELECT count(*) FROM documents WHERE case_id = $1", case_id
+ )
+ result.append({
+ "case_number": c["case_number"],
+ "title": c["title"],
+ "status": c["status"],
+ "expected_outcome": c.get("expected_outcome", ""),
+ "committee_type": c.get("committee_type", ""),
+ "hearing_date": str(c["hearing_date"]) if c.get("hearing_date") else "",
+ "document_count": doc_count,
+ "gitea_url": f"https://gitea.nautilus.marcusgroup.org/cases/{c['case_number']}",
+ })
+ return result
# ── Paperclip Integration API ─────────────────────────────────────
@@ -546,6 +569,187 @@ async def api_document_text(doc_id: str):
return {"doc_id": doc_id, "text": text}
+# ── Integration Endpoints — Gitea & Paperclip ────────────────────
+
+
+DOC_TYPE_NAMES = {
+ "appeal": "כתב-ערר",
+ "response": "תשובת",
+ "protocol": "פרוטוקול-דיון",
+ "plan": "תכנית",
+ "decision": "החלטה",
+ "court_decision": "פסק-דין",
+ "permit": "היתר",
+ "appraisal": "שומה",
+ "exhibit": "נספח",
+ "objection": "התנגדות",
+ "reference": "מסמך-עזר",
+}
+
+
+def generate_doc_filename(doc_type: str, case_number: str, party_name: str = "", ext: str = ".pdf") -> str:
+ """Generate a clear Hebrew filename for a document."""
+ base = DOC_TYPE_NAMES.get(doc_type, doc_type)
+ parts = [base]
+ if party_name:
+ safe_party = re.sub(r"[^\w\u0590-\u05FF\s]", "", party_name).strip().replace(" ", "-")
+ parts.append(safe_party)
+ parts.append(case_number)
+ return "-".join(parts) + ext
+
+
+class GiteaRepoRequest(BaseModel):
+ case_number: str
+ title: str
+ description: str = ""
+
+
+@app.post("/api/integrations/gitea/create-repo")
+async def api_gitea_create_repo(req: GiteaRepoRequest):
+ """Create a Gitea repo in the 'cases' org and link it to the local case directory."""
+ try:
+ repo = await create_repo(req.case_number, req.title, req.description)
+ except Exception as e:
+ raise HTTPException(502, f"Gitea error: {e}")
+
+ clone_url = repo.get("clone_url") or repo.get("html_url", "")
+ case_dir = config.CASES_DIR / req.case_number
+
+ pushed = False
+ if case_dir.exists():
+ pushed = setup_remote_and_push(case_dir, clone_url)
+
+ return {
+ "repo_url": repo.get("html_url", ""),
+ "clone_url": clone_url,
+ "pushed": pushed,
+ }
+
+
+class PaperclipProjectRequest(BaseModel):
+ case_number: str
+ title: str
+ description: str = ""
+ appeal_type: str = "רישוי"
+
+
+@app.post("/api/integrations/paperclip/create-project")
+async def api_paperclip_create_project(req: PaperclipProjectRequest):
+ """Create a project in Paperclip's embedded DB."""
+ try:
+ project = await pc_create_project(
+ case_number=req.case_number,
+ title=req.title,
+ description=req.description,
+ appeal_type=req.appeal_type,
+ )
+ except Exception as e:
+ raise HTTPException(502, f"Paperclip error: {e}")
+ return project
+
+
+@app.post("/api/cases/{case_number}/documents/upload-tagged")
+async def api_upload_tagged_document(
+ case_number: str,
+ file: UploadFile = File(...),
+ doc_type: str = Form("auto"),
+ party_name: str = Form(""),
+ title: str = Form(""),
+):
+ """Upload a document to a case with tagging and auto-rename."""
+ case = await db.get_case_by_number(case_number)
+ if not case:
+ raise HTTPException(404, f"תיק {case_number} לא נמצא")
+
+ if not file.filename:
+ raise HTTPException(400, "No filename provided")
+
+ ext = Path(file.filename).suffix.lower()
+ if ext not in ALLOWED_EXTENSIONS:
+ raise HTTPException(400, f"סוג קובץ לא נתמך: {ext}")
+
+ content = await file.read()
+ if len(content) > MAX_FILE_SIZE:
+ raise HTTPException(400, f"קובץ גדול מדי. מקסימום: {MAX_FILE_SIZE // (1024*1024)}MB")
+
+ # Generate smart filename
+ new_filename = generate_doc_filename(doc_type, case_number, party_name, ext)
+
+ # Save to case directory
+ case_dir = config.CASES_DIR / case_number / "documents"
+ case_dir.mkdir(parents=True, exist_ok=True)
+ dest = case_dir / new_filename
+
+ # Handle duplicates
+ counter = 1
+ while dest.exists():
+ stem = new_filename.rsplit(".", 1)[0]
+ dest = case_dir / f"{stem}-{counter}{ext}"
+ counter += 1
+
+ dest.write_bytes(content)
+
+ # Create document record
+ case_id = UUID(case["id"])
+ doc_title = title or new_filename.rsplit(".", 1)[0].replace("-", " ")
+ doc = await db.create_document(
+ case_id=case_id,
+ doc_type=doc_type if doc_type != "auto" else "reference",
+ title=doc_title,
+ file_path=str(dest),
+ )
+
+ # Process in background
+ task_id = str(uuid4())
+ _progress[task_id] = {"status": "queued", "filename": new_filename}
+ asyncio.create_task(_process_tagged_document(task_id, dest, case_number, case_id, UUID(doc["id"]), doc_type, new_filename))
+
+ return {
+ "task_id": task_id,
+ "filename": new_filename,
+ "original_name": file.filename,
+ "doc_type": doc_type,
+ }
+
+
+async def _process_tagged_document(task_id: str, dest: Path, case_number: str, case_id: UUID, doc_id: UUID, doc_type: str, display_name: str):
+ """Process an uploaded tagged document in the background."""
+ try:
+ _progress[task_id] = {"status": "processing", "filename": display_name, "step": "extracting"}
+ result = await processor.process_document(doc_id, case_id)
+
+ # Git commit + push
+ repo_dir = config.CASES_DIR / case_number
+ if repo_dir.exists():
+ env = {
+ "GIT_AUTHOR_NAME": "Ezer Mishpati", "GIT_AUTHOR_EMAIL": "legal@local",
+ "GIT_COMMITTER_NAME": "Ezer Mishpati", "GIT_COMMITTER_EMAIL": "legal@local",
+ "PATH": "/usr/bin:/bin",
+ }
+ doc_type_hebrew = DOC_TYPE_NAMES.get(doc_type, doc_type)
+ subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True)
+ subprocess.run(
+ ["git", "commit", "-m", f"הוספת {doc_type_hebrew}: {display_name}"],
+ cwd=repo_dir, capture_output=True, env=env,
+ )
+ # Try to push to Gitea (non-blocking)
+ subprocess.run(["git", "push"], cwd=repo_dir, capture_output=True, env={
+ **env,
+ "GIT_TERMINAL_PROMPT": "0",
+ })
+
+ _progress[task_id] = {
+ "status": "completed",
+ "filename": display_name,
+ "result": result,
+ "case_number": case_number,
+ "doc_type": doc_type,
+ }
+ except Exception as e:
+ logger.exception("Processing failed for %s", display_name)
+ _progress[task_id] = {"status": "failed", "error": str(e), "filename": display_name}
+
+
# ── Background Processing ─────────────────────────────────────────
diff --git a/web/gitea_client.py b/web/gitea_client.py
new file mode 100644
index 0000000..580b438
--- /dev/null
+++ b/web/gitea_client.py
@@ -0,0 +1,98 @@
+"""Gitea REST API client — create repos in the 'cases' org and push."""
+
+from __future__ import annotations
+
+import logging
+import os
+import subprocess
+from pathlib import Path
+
+import httpx
+
+logger = logging.getLogger(__name__)
+
+GITEA_ORG = "cases"
+
+
+def _host() -> str:
+ return os.environ.get("GITEA_HOST", "https://gitea.nautilus.marcusgroup.org")
+
+
+def _token() -> str:
+ return os.environ.get("GITEA_ACCESS_TOKEN", "")
+
+
+async def create_repo(case_number: str, title: str, description: str = "") -> dict:
+ """Create a private repo in the 'cases' org on Gitea."""
+ repo_name = case_number # e.g. "1130-25"
+ async with httpx.AsyncClient(verify=False, timeout=30) as client:
+ resp = await client.post(
+ f"{_host()}/api/v1/orgs/{GITEA_ORG}/repos",
+ headers={"Authorization": f"token {_token()}"},
+ json={
+ "name": repo_name,
+ "description": f"ערר {case_number} — {title}"[:255],
+ "private": True,
+ "auto_init": False,
+ },
+ )
+ if resp.status_code == 409:
+ # Repo already exists — fetch it
+ resp2 = await client.get(
+ f"{_host()}/api/v1/repos/{GITEA_ORG}/{repo_name}",
+ headers={"Authorization": f"token {_token()}"},
+ )
+ resp2.raise_for_status()
+ return resp2.json()
+ resp.raise_for_status()
+ return resp.json()
+
+
+def setup_remote_and_push(case_dir: str | Path, repo_clone_url: str) -> bool:
+ """Add Gitea as remote 'origin' (or update it) and push."""
+ case_dir = Path(case_dir)
+ if not (case_dir / ".git").exists():
+ return False
+
+ env = {
+ "GIT_AUTHOR_NAME": "Ezer Mishpati",
+ "GIT_AUTHOR_EMAIL": "legal@local",
+ "GIT_COMMITTER_NAME": "Ezer Mishpati",
+ "GIT_COMMITTER_EMAIL": "legal@local",
+ "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
+ }
+
+ # Inject token into clone URL for auth
+ auth_url = repo_clone_url.replace("https://", f"https://chaim:{_token()}@")
+
+ # Check if remote exists
+ result = subprocess.run(
+ ["git", "remote", "get-url", "origin"],
+ cwd=case_dir, capture_output=True, text=True,
+ )
+ if result.returncode == 0:
+ subprocess.run(
+ ["git", "remote", "set-url", "origin", auth_url],
+ cwd=case_dir, capture_output=True, env=env,
+ )
+ else:
+ subprocess.run(
+ ["git", "remote", "add", "origin", auth_url],
+ cwd=case_dir, capture_output=True, env=env,
+ )
+
+ # Push
+ push_result = subprocess.run(
+ ["git", "push", "-u", "origin", "main"],
+ cwd=case_dir, capture_output=True, text=True, env=env,
+ )
+ if push_result.returncode != 0:
+ # Try master branch
+ push_result = subprocess.run(
+ ["git", "push", "-u", "origin", "master"],
+ cwd=case_dir, capture_output=True, text=True, env=env,
+ )
+ if push_result.returncode != 0:
+ logger.warning("Git push failed: %s", push_result.stderr)
+ return False
+ return True
diff --git a/web/paperclip_client.py b/web/paperclip_client.py
new file mode 100644
index 0000000..7d45ee3
--- /dev/null
+++ b/web/paperclip_client.py
@@ -0,0 +1,77 @@
+"""Paperclip project creation via direct DB access (embedded PostgreSQL)."""
+
+from __future__ import annotations
+
+import logging
+import uuid
+
+import asyncpg
+
+logger = logging.getLogger(__name__)
+
+PAPERCLIP_DB_URL = "postgresql://paperclip:paperclip@127.0.0.1:54329/paperclip"
+
+# Company IDs from Paperclip DB
+COMPANIES = {
+ "licensing": "42a7acd0-30c5-4cbd-ac97-7424f65df294", # CMP — רישוי ובניה
+ "betterment": "8639e837-4c9d-47fa-a76b-95788d651896", # CMPA — היטלי השבחה
+}
+
+APPEAL_TYPE_TO_COMPANY = {
+ "רישוי": "licensing",
+ "licensing": "licensing",
+ "היטל השבחה": "betterment",
+ "betterment_levy": "betterment",
+ "פיצויים": "betterment",
+ "compensation": "betterment",
+}
+
+
+def _get_company_id(appeal_type: str) -> str:
+ key = APPEAL_TYPE_TO_COMPANY.get(appeal_type, "licensing")
+ return COMPANIES[key]
+
+
+async def create_project(
+ case_number: str,
+ title: str,
+ description: str = "",
+ appeal_type: str = "רישוי",
+ color: str = "#6366f1",
+) -> dict:
+ """Create a project in the Paperclip embedded DB."""
+ company_id = _get_company_id(appeal_type)
+ project_id = str(uuid.uuid4())
+
+ conn = await asyncpg.connect(PAPERCLIP_DB_URL)
+ try:
+ await conn.execute(
+ """INSERT INTO projects (id, company_id, name, description, status, color)
+ VALUES ($1, $2::uuid, $3, $4, 'backlog', $5)
+ ON CONFLICT DO NOTHING""",
+ project_id, company_id, f"ערר {case_number} — {title}"[:200], description[:500] if description else "", color,
+ )
+ return {
+ "id": project_id,
+ "company_id": company_id,
+ "name": f"ערר {case_number} — {title}",
+ "url": f"https://pc.nautilus.marcusgroup.org/{'CMP' if 'licensing' in appeal_type or appeal_type == 'רישוי' else 'CMPA'}/projects/{project_id}/issues",
+ }
+ finally:
+ await conn.close()
+
+
+async def get_project_url(case_number: str) -> str | None:
+ """Find existing Paperclip project for a case number."""
+ conn = await asyncpg.connect(PAPERCLIP_DB_URL)
+ try:
+ row = await conn.fetchrow(
+ "SELECT id, company_id FROM projects WHERE name LIKE $1",
+ f"%{case_number}%",
+ )
+ if row:
+ prefix = "CMP" if row["company_id"] == uuid.UUID(COMPANIES["licensing"]) else "CMPA"
+ return f"https://pc.nautilus.marcusgroup.org/{prefix}/projects/{row['id']}/issues"
+ return None
+ finally:
+ await conn.close()
diff --git a/web/static/index.html b/web/static/index.html
index 3f056e9..6bb1f24 100644
--- a/web/static/index.html
+++ b/web/static/index.html
@@ -3,7 +3,7 @@
-עוזר משפטי — העלאת מסמכים
+עוזר משפטי — ניהול תיקים
-
-
-
-
-
-
📄
-
גרור קבצים לכאן או לחץ לבחירה
-
PDF, DOCX, RTF, TXT — עד 50MB
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
אין קבצים ממתינים
+
+
+
+
1. פרטי תיק
+
2. צדדים
+
3. לוח זמנים
+
4. סיכום ויצירה
+
+
+
+
-
-
-
- העלה מסמכים באמצעות אזור הגרירה
- הקבצים יופיעו כאן לסיווג ועיבוד
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ - ◯ יצירת תיק במסד הנתונים
+ - ◯ יצירת Repository ב-Gitea
+ - ◯ יצירת פרויקט ב-Paperclip
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
גרור קבצים לכאן או לחץ לבחירה
+
PDF, DOCX, RTF, TXT — עד 50MB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📄
+
גרור קבצים לכאן או לחץ לבחירה
+
PDF, DOCX, RTF, TXT — עד 50MB
+
+
+
+
+
-
+
תיקים: —
מסמכים: —
קטעים: —
@@ -456,317 +437,588 @@ header .subtitle {