From 2fe73fcce1b5638b104d4e1275c293a3c1e9f7ac Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 4 May 2026 06:26:00 +0000 Subject: [PATCH] feat(settings): add PATCH env + Coolify redeploy endpoints Co-Authored-By: Claude Sonnet 4.6 --- web/app.py | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/web/app.py b/web/app.py index d1ba5b2..7418dc5 100644 --- a/web/app.py +++ b/web/app.py @@ -2672,6 +2672,107 @@ async def api_mcp_env(): } +class McpEnvUpdateRequest(BaseModel): + value: Any + + +@app.patch("/api/settings/mcp/env/{key}") +async def api_mcp_env_update(key: str, req: McpEnvUpdateRequest): + """Update a non-secret env var in Infisical. Requires redeploy to take effect.""" + spec = ENV_CATALOG.get(key) + if spec is None: + raise HTTPException(404, f"Unknown env key: {key}") + if spec.is_secret: + raise HTTPException(400, f"Cannot edit secret: {key}") + if not spec.is_editable: + raise HTTPException(400, f"Read-only: {key}") + try: + coerced = coerce(spec, req.value) + except ValueError as e: + raise HTTPException(400, str(e)) + + client = _infisical_client() + if client is None: + raise HTTPException(503, "Infisical not configured") + project_id, env, path = _infisical_ctx() + str_value = "true" if coerced is True else ( + "false" if coerced is False else str(coerced) + ) + try: + # SDK pattern: try update, fall back to create if missing. + # NOTE: exact method may vary by infisical-python version. The + # canonical method is `update_secret_by_name`; if your version + # uses `secrets.update`, replace accordingly. + try: + client.update_secret_by_name( + project_id=project_id, + environment_slug=env, + secret_path=path, + secret_name=key, + secret_value=str_value, + ) + except Exception: + client.create_secret_by_name( + project_id=project_id, + environment_slug=env, + secret_path=path, + secret_name=key, + secret_value=str_value, + ) + except Exception as e: + logger.exception("infisical_write_failed key=%s", key) + raise HTTPException(502, f"Infisical write failed: {e}") + + logger.info( + "mcp_env_update key=%s value=%s", + key, + "[masked]" if spec.is_secret else str_value, + ) + return { + "ok": True, + "key": key, + "saved_value": str_value, + "requires_redeploy": True, + "message": "נשמר ב-Infisical. נדרש redeploy כדי שיכנס לתוקף בקונטיינר.", + } + + +@app.post("/api/settings/mcp/env/redeploy") +async def api_mcp_env_redeploy(): + """Trigger Coolify redeploy of the legal-ai app.""" + import httpx + coolify_url = os.environ.get("COOLIFY_URL", "http://158.178.131.193:8000") + coolify_token = os.environ.get("COOLIFY_API_TOKEN", "") + app_uuid = os.environ.get("COOLIFY_APP_UUID", "gyjo0mtw2c42ej3xxvbz8zio") + if not coolify_token: + raise HTTPException(503, "COOLIFY_API_TOKEN not configured") + + async with httpx.AsyncClient(timeout=30.0) as http: + try: + resp = await http.post( + f"{coolify_url}/api/v1/deploy", + params={"uuid": app_uuid, "force": "false"}, + headers={"Authorization": f"Bearer {coolify_token}"}, + ) + except Exception as e: + raise HTTPException(502, f"Coolify unreachable: {e}") + if resp.status_code >= 400: + raise HTTPException( + 502, f"Coolify deploy failed: {resp.status_code} {resp.text}" + ) + data = resp.json() if resp.content else {} + deployment_uuid = ( + data.get("deployment_uuid") + or (data.get("deployments") or [{}])[0].get("deployment_uuid") + ) + logger.info("mcp_env_redeploy triggered uuid=%s", deployment_uuid) + return { + "ok": True, + "deployment_uuid": deployment_uuid, + "message": "Redeploy הופעל. הקונטיינר יחזור תוך 2-4 דקות.", + } + + # ── Settings: Tag → Company Mappings ────────────────────────────── @app.get("/api/settings/paperclip-companies")