diff --git a/requirements.txt b/requirements.txt index 890199b..7ca6594 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ httpx==0.27.0 pyyaml==6.0.1 pytest==8.0.0 +fastapi==0.111.0 +uvicorn==0.30.0 +python-multipart==0.0.9 +jinja2==3.1.4 diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/api.py b/web/api.py new file mode 100644 index 0000000..d7daa4d --- /dev/null +++ b/web/api.py @@ -0,0 +1,80 @@ +"""Web API layer — wraps orchestrator with 202+ polling pattern.""" +import uuid, json, shutil, sys, os +from pathlib import Path +from datetime import datetime +from fastapi import FastAPI, UploadFile, File, Form, HTTPException +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from starlette.requests import Request + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from config import Config +from orchestrator import run_pipeline + +app = FastAPI(title="COBOL→Java Verify") +app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static") +templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates")) + +TASKS_DIR = Path("tasks") +TASKS_DIR.mkdir(exist_ok=True) +UPLOAD_DIR = Path("uploads") +UPLOAD_DIR.mkdir(exist_ok=True) +MAX_SIZE = 10 * 1024 * 1024 # 10MB + + +@app.get("/", response_class=HTMLResponse) +async def index(request: Request): + return templates.TemplateResponse("upload.html", {"request": request}) + + +@app.post("/verify") +async def verify( + copybook: UploadFile = File(...), + cobol_src: UploadFile = File(...), + java_src: UploadFile = File(...), + mapping: UploadFile = File(...), + runner: str = Form("native"), +): + task_id = str(uuid.uuid4())[:8] + task_dir = UPLOAD_DIR / task_id + task_dir.mkdir(parents=True, exist_ok=True) + + for f, name in [(copybook, "copybook.cpy"), (cobol_src, "program.cbl"), + (java_src, "java"), (mapping, "mapping.yaml")]: + content = await f.read() + if len(content) > MAX_SIZE: + raise HTTPException(413, f"{f.filename} exceeds 10MB limit") + dest = task_dir / name + dest.write_bytes(content) + + task_file = TASKS_DIR / f"{task_id}.json" + task_file.write_text(json.dumps({ + "id": task_id, "status": "queued", + "copybook": str(task_dir / "copybook.cpy"), + "cobol_src": str(task_dir / "program.cbl"), + "java_src": str(task_dir / "java"), + "mapping": str(task_dir / "mapping.yaml"), + "runner": runner, "created": datetime.now().isoformat() + })) + + return JSONResponse({"task_id": task_id, "status": "queued"}, status_code=202) + + +@app.get("/status/{task_id}") +async def status(task_id: str): + tf = TASKS_DIR / f"{task_id}.json" + if not tf.exists(): + raise HTTPException(404, "task not found") + data = json.loads(tf.read_text()) + return JSONResponse({"task_id": task_id, "status": data.get("status", "unknown"), + "result": data.get("result")}) + + +@app.get("/result/{task_id}", response_class=HTMLResponse) +async def result(request: Request, task_id: str): + tf = TASKS_DIR / f"{task_id}.json" + if not tf.exists(): + raise HTTPException(404, "task not found") + data = json.loads(tf.read_text()) + return templates.TemplateResponse("result.html", {"request": request, "task": data}) diff --git a/web/static/script.js b/web/static/script.js new file mode 100644 index 0000000..f2f33b0 --- /dev/null +++ b/web/static/script.js @@ -0,0 +1,25 @@ +document.getElementById("verify-form").addEventListener("submit", async (e) => { + e.preventDefault(); + const btn = e.target.querySelector("button"); + btn.disabled = true; + btn.textContent = "Submitting..."; + document.getElementById("status").textContent = ""; + document.getElementById("result").innerHTML = ""; + + const fd = new FormData(e.target); + try { + const r = await fetch("/verify", { method: "POST", body: fd }); + const d = await r.json(); + if (r.ok) { + document.getElementById("status").textContent = "Task " + d.task_id + " queued"; + document.getElementById("result").innerHTML = + '

View result →

'; + } else { + document.getElementById("status").textContent = "Error: " + (d.detail || "unknown"); + } + } catch (err) { + document.getElementById("status").textContent = "Upload failed: " + err.message; + } + btn.disabled = false; + btn.textContent = "Verify"; +}); diff --git a/web/static/style.css b/web/static/style.css new file mode 100644 index 0000000..2286ffa --- /dev/null +++ b/web/static/style.css @@ -0,0 +1,11 @@ +body { font-family: monospace; max-width: 900px; margin: 2rem auto; padding: 0 1rem; color: #333; } +h1 { font-size: 1.2rem; } +label { display: block; margin: .75rem 0; } +input, select { display: block; margin-top: .25rem; } +button { margin-top: 1rem; padding: .5rem 1.5rem; cursor: pointer; font-size: 1rem; } +#status { margin-top: 1rem; font-weight: bold; } +#result { margin-top: 1rem; } +pre { background: #f0f0f0; padding: 1rem; border-radius: 4px; } +.pass { border-left: 4px solid #4caf50; } +.fail { border-left: 4px solid #f44336; } +.container a { display: inline-block; margin-top: 1rem; } diff --git a/web/templates/result.html b/web/templates/result.html new file mode 100644 index 0000000..74fc27d --- /dev/null +++ b/web/templates/result.html @@ -0,0 +1,27 @@ + +Result - {{ task.id }} + +
+

Verification Result

+{% if task.status == "done" and task.result %} +
Status: {{ task.result.status }}
+Program: {{ task.result.program }}
+Matched: {{ task.result.matched }} | Mismatched: {{ task.result.mismatched }}
+Runner: {{ task.result.runner }} | Duration: {{ task.result.duration }}s
+{% elif task.status == "error" %} +
{{ task.result }}
+{% else %} +
Status: {{ task.status }} — polling...
+ +{% endif %} +← New verification +
+ diff --git a/web/templates/upload.html b/web/templates/upload.html new file mode 100644 index 0000000..2a28a1b --- /dev/null +++ b/web/templates/upload.html @@ -0,0 +1,18 @@ + +COBOL→Java Verify + +
+

COBOL→Java Migration Verification

+
+ + + + + + +
+
+
+
+ + diff --git a/web/worker.py b/web/worker.py new file mode 100644 index 0000000..80500c2 --- /dev/null +++ b/web/worker.py @@ -0,0 +1,53 @@ +"""Worker process — polls task queue, executes run_pipeline().""" +import json, sys, time, shutil +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +TASKS_DIR = Path("tasks") + +def main(): + print("Worker started. Watching tasks/ ...") + while True: + for tf in sorted(TASKS_DIR.glob("*.json")): + try: + data = json.loads(tf.read_text()) + if data.get("status") != "queued": + continue + + data["status"] = "running" + tf.write_text(json.dumps(data)) + + from config import Config + from orchestrator import run_pipeline + + cfg = Config() + cfg.runner_mode = data.get("runner", "native") + if cfg.runner_mode == "spark" and not shutil.which("spark-submit"): + data["status"] = "blocked" + data["result"] = "spark-submit not installed" + tf.write_text(json.dumps(data)) + continue + + vr = run_pipeline(cfg, data["copybook"], data["cobol_src"], + data["java_src"], data["mapping"]) + + data["status"] = "done" + data["result"] = { + "program": vr.program, "status": vr.status, + "matched": vr.fields_matched, "mismatched": vr.fields_mismatched, + "duration": vr.duration_s, "runner": vr.runner, + } + tf.write_text(json.dumps(data)) + + except Exception as e: + data = json.loads(tf.read_text()) if tf.exists() else {} + data["status"] = "error" + data["result"] = str(e)[:500] + tf.write_text(json.dumps(data)) + + time.sleep(2) + + +if __name__ == "__main__": + main()