-
Notifications
You must be signed in to change notification settings - Fork 0
Add standalone P&ID designer app #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
609b0a5
cd9402f
8dd4212
d0e413f
64b27b5
bb732d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| name: P&ID Designer CI | ||
|
|
||
| 'on': | ||
| push: | ||
| paths: | ||
| - 'pid-designer/**' | ||
| - '.github/workflows/pid-designer-ci.yml' | ||
| pull_request: | ||
| paths: | ||
| - 'pid-designer/**' | ||
| - '.github/workflows/pid-designer-ci.yml' | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| frontend: | ||
| name: Frontend (TypeScript build) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20.x' | ||
| cache: 'npm' | ||
| cache-dependency-path: pid-designer/frontend/package-lock.json | ||
|
|
||
| - name: Install dependencies | ||
| working-directory: pid-designer/frontend | ||
| run: npm ci || npm install | ||
|
|
||
| - name: TypeScript build | ||
| working-directory: pid-designer/frontend | ||
| run: npm run build | ||
|
|
||
| backend: | ||
| name: Backend (import check) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.11' | ||
|
|
||
| - name: Install dependencies | ||
| run: pip install "fastapi>=0.128" "uvicorn[standard]>=0.34" "pydantic>=2.12" | ||
|
|
||
| - name: Verify imports | ||
| working-directory: pid-designer | ||
| run: python3 -c "import backend.main; print('backend imports OK')" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| node_modules/ | ||
| __pycache__/ | ||
| *.pyc | ||
| *.pyo | ||
| .vite/ | ||
| dist/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| """Standalone FastAPI backend for the P&ID Designer. | ||
|
|
||
| Run with: | ||
| cd pid-designer | ||
| uvicorn backend.main:app --reload --port 8001 | ||
| """ | ||
|
|
||
| from fastapi import FastAPI | ||
| from fastapi.middleware.cors import CORSMiddleware | ||
|
|
||
| from backend.routers import pid | ||
|
|
||
| app = FastAPI(title="P&ID Designer API", version="1.0.0") | ||
|
|
||
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=[ | ||
| "http://localhost:5173", | ||
| "http://localhost:5174", | ||
| "http://127.0.0.1:5173", | ||
| "http://127.0.0.1:5174", | ||
| ], | ||
| allow_credentials=True, | ||
| allow_methods=["*"], | ||
| allow_headers=["*"], | ||
| ) | ||
|
|
||
| app.include_router(pid.router) | ||
|
|
||
|
|
||
| @app.get("/api/health") | ||
| async def health(): | ||
| return {"status": "healthy"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| """PID diagram persistence and git-backed version control.""" | ||
|
|
||
| import json | ||
| import subprocess | ||
| from pathlib import Path | ||
|
|
||
| from fastapi import APIRouter, HTTPException | ||
| from pydantic import BaseModel | ||
| from typing import Any | ||
|
|
||
| router = APIRouter(prefix="/api/pid", tags=["pid"]) | ||
|
|
||
| REPO_ROOT = Path(__file__).resolve().parents[2] | ||
| DIAGRAM_PATH = REPO_ROOT / "diagrams" / "pid_main.json" | ||
|
|
||
| def _git_root() -> Path: | ||
| r = subprocess.run(["git", "rev-parse", "--show-toplevel"], cwd=REPO_ROOT, | ||
| capture_output=True, text=True) | ||
| return Path(r.stdout.strip()) | ||
|
|
||
| GIT_ROOT = _git_root() | ||
| # Path used in `git show <hash>:<path>` — must be relative to the git repo root. | ||
| GIT_DIAGRAM_PATH = str(DIAGRAM_PATH.relative_to(GIT_ROOT)) | ||
|
|
||
|
|
||
| def _run_git(*args: str) -> str: | ||
| """Run a git command in the repo root, return stdout. Raises RuntimeError on failure.""" | ||
| result = subprocess.run( | ||
| ["git", *args], | ||
| cwd=REPO_ROOT, | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, | ||
| ) | ||
| if result.returncode != 0: | ||
| raise RuntimeError(result.stderr.strip() or result.stdout.strip()) | ||
| return result.stdout | ||
|
|
||
|
|
||
| def _read_diagram() -> dict: | ||
| if not DIAGRAM_PATH.exists(): | ||
| return {"nodes": [], "edges": []} | ||
| return json.loads(DIAGRAM_PATH.read_text()) | ||
|
|
||
|
|
||
| def _write_diagram(data: dict) -> None: | ||
| DIAGRAM_PATH.parent.mkdir(parents=True, exist_ok=True) | ||
| DIAGRAM_PATH.write_text(json.dumps(data, indent=2)) | ||
|
|
||
|
|
||
| # ── Request models ──────────────────────────────────────────────────────────── | ||
|
|
||
| class DiagramPayload(BaseModel): | ||
| nodes: list[Any] | ||
| edges: list[Any] | ||
|
|
||
|
|
||
| class CheckpointPayload(BaseModel): | ||
| nodes: list[Any] | ||
| edges: list[Any] | ||
| title: str | ||
| description: str = "" | ||
|
|
||
|
|
||
| # ── Endpoints ───────────────────────────────────────────────────────────────── | ||
|
|
||
| @router.get("/load") | ||
| async def load_diagram(): | ||
| """Load the current diagram from disk.""" | ||
| return _read_diagram() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Load crashes on bad JSONMedium Severity
Reviewed by Cursor Bugbot for commit d0e413f. Configure here. |
||
|
|
||
|
|
||
| @router.post("/autosave") | ||
| async def autosave_diagram(payload: DiagramPayload): | ||
| """Write diagram to disk silently — no git commit.""" | ||
| _write_diagram({"nodes": payload.nodes, "edges": payload.edges}) | ||
| return {"ok": True} | ||
|
|
||
|
|
||
| @router.post("/pull") | ||
| async def pull_latest(): | ||
| """Git pull then return the updated diagram.""" | ||
| try: | ||
| _run_git("pull") | ||
| except RuntimeError as e: | ||
| raise HTTPException(status_code=500, detail=f"git pull failed: {e}") | ||
| return _read_diagram() | ||
|
|
||
|
|
||
| @router.post("/checkpoint") | ||
| async def checkpoint(payload: CheckpointPayload): | ||
| """Write diagram, commit, and push with a user-provided title + description.""" | ||
| _write_diagram({"nodes": payload.nodes, "edges": payload.edges}) | ||
|
|
||
| commit_message = payload.title.strip() | ||
| if payload.description.strip(): | ||
| commit_message += f"\n\n{payload.description.strip()}" | ||
|
|
||
| try: | ||
| rel_path = str(DIAGRAM_PATH.relative_to(REPO_ROOT)) | ||
| _run_git("add", rel_path) | ||
| _run_git("commit", "-m", commit_message) | ||
| _run_git("push") | ||
| except RuntimeError as e: | ||
| raise HTTPException(status_code=500, detail=f"git operation failed: {e}") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checkpoint fails unchanged fileMedium Severity After autosave has already written the same JSON, Reviewed by Cursor Bugbot for commit 64b27b5. Configure here. |
||
|
|
||
| commit_hash = _run_git("rev-parse", "--short", "HEAD").strip() | ||
| return {"ok": True, "commit": commit_hash} | ||
|
|
||
|
|
||
| @router.get("/history") | ||
| async def get_history(): | ||
| """Return the last 10 commits that touched diagrams/pid_main.json.""" | ||
| rel_path = str(DIAGRAM_PATH.relative_to(REPO_ROOT)) | ||
| try: | ||
| log = _run_git("log", "--format=%H|%s|%aI", "-10", "--", rel_path) | ||
| except RuntimeError as e: | ||
| raise HTTPException(status_code=500, detail=f"git log failed: {e}") | ||
|
|
||
| entries = [] | ||
| for line in log.splitlines(): | ||
| line = line.strip() | ||
| if not line: | ||
| continue | ||
| parts = line.split("|", 2) | ||
| if len(parts) == 3: | ||
| entries.append({"hash": parts[0], "title": parts[1], "timestamp": parts[2]}) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. History breaks on pipe titlesMedium Severity History lines join hash, subject, and ISO time with Reviewed by Cursor Bugbot for commit cd9402f. Configure here. |
||
| return entries | ||
|
|
||
|
|
||
| @router.get("/version/{commit_hash}") | ||
| async def get_version(commit_hash: str): | ||
| """Return the diagram snapshot at a specific commit.""" | ||
| try: | ||
| content = _run_git("show", f"{commit_hash}:{GIT_DIAGRAM_PATH}") | ||
| except RuntimeError as e: | ||
| raise HTTPException(status_code=404, detail=f"Version not found: {e}") | ||
| try: | ||
| return json.loads(content) | ||
| except json.JSONDecodeError: | ||
| raise HTTPException(status_code=500, detail="Snapshot at that commit is not valid JSON") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| #!/usr/bin/env bash | ||
| set -e | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| cd "$SCRIPT_DIR" | ||
|
|
||
| cleanup() { | ||
| echo "" | ||
| echo "Shutting down..." | ||
| kill "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null | ||
| wait "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null | ||
| } | ||
| trap cleanup EXIT INT TERM | ||
|
|
||
| # Install frontend deps if needed | ||
| if [ ! -d "frontend/node_modules" ]; then | ||
| echo "Installing frontend dependencies..." | ||
| (cd frontend && npm install) | ||
| fi | ||
|
|
||
| echo "Starting backend on http://localhost:8001" | ||
| uvicorn backend.main:app --reload --port 8001 & | ||
| BACKEND_PID=$! | ||
|
|
||
| echo "Starting frontend on http://localhost:5174" | ||
| (cd frontend && npm run dev) & | ||
| FRONTEND_PID=$! | ||
|
|
||
| wait "$BACKEND_PID" "$FRONTEND_PID" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>P&ID Designer</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Git root resolved at import
Medium Severity
_git_root()ignoresgitexit status and module-levelGIT_DIAGRAM_PATHis computed at import; a failedrev-parsecan make router import crash or break version restore paths.Reviewed by Cursor Bugbot for commit d0e413f. Configure here.