From 7d94ec0d18b46893e23680cf8438109a34cc2a10 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sun, 22 Feb 2026 16:55:03 +0100
Subject: [PATCH] feat: promote/sync/rebuild UI, operations page, bidirectional sync, lifecycle ops

---
 app/ops_runner.py |   31 ++++++++++++++++++++++++++++++-
 1 files changed, 30 insertions(+), 1 deletions(-)

diff --git a/app/ops_runner.py b/app/ops_runner.py
index 226fdaa..d9a460a 100644
--- a/app/ops_runner.py
+++ b/app/ops_runner.py
@@ -39,10 +39,11 @@
         data = json.loads(result["output"])
         return {"success": True, "data": data, "error": ""}
     except json.JSONDecodeError as exc:
+        raw = result["output"][:500]
         return {
             "success": False,
             "data": None,
-            "error": f"Failed to parse JSON: {exc}\nRaw: {result['output'][:500]}",
+            "error": f"Failed to parse JSON: {exc}\nRaw: {raw}",
         }
 
 
@@ -72,12 +73,40 @@
     return await _run_exec(_NSENTER_PREFIX + [OPS_CLI] + args, timeout=timeout)
 
 
+async def run_ops_host_json(args: list[str], timeout: int = _DEFAULT_TIMEOUT) -> dict:
+    """Run the ops CLI on the host via nsenter with --json and return parsed JSON."""
+    result = await run_ops_host(args + ["--json"], timeout=timeout)
+    if not result["success"]:
+        return {"success": False, "data": None, "error": result["error"] or result["output"]}
+    try:
+        data = json.loads(result["output"])
+        return {"success": True, "data": data, "error": ""}
+    except json.JSONDecodeError as exc:
+        raw = result["output"][:500]
+        return {
+            "success": False,
+            "data": None,
+            "error": f"Failed to parse JSON: {exc}\nRaw: {raw}",
+        }
+
+
 async def stream_ops_host(args: list[str], timeout: int = _DEFAULT_TIMEOUT) -> AsyncGenerator[str, None]:
     """Stream ops CLI output from the host via nsenter."""
     async for line in _stream_exec(_NSENTER_PREFIX + [OPS_CLI] + args, timeout=timeout):
         yield line
 
 
+async def run_command_host(args: list[str], timeout: int = _DEFAULT_TIMEOUT) -> dict:
+    """Run an arbitrary command on the host via nsenter."""
+    return await _run_exec(_NSENTER_PREFIX + args, timeout=timeout)
+
+
+async def stream_command_host(args: list[str], timeout: int = _DEFAULT_TIMEOUT) -> AsyncGenerator[str, None]:
+    """Stream arbitrary command output from the host via nsenter."""
+    async for line in _stream_exec(_NSENTER_PREFIX + args, timeout=timeout):
+        yield line
+
+
 # ---------------------------------------------------------------------------
 # Internal helpers
 # ---------------------------------------------------------------------------

--
Gitblit v1.3.1