fixes and improvements

This commit is contained in:
2026-05-07 13:48:10 +02:00
parent bba23165ab
commit efc9b247c7
8 changed files with 344 additions and 245 deletions
@@ -55,7 +55,9 @@ def post_machine(
store: SettingsStore = Depends(get_settings_store),
) -> dict[str, Any]:
saved = store.upsert_machine(machine.model_dump(exclude_none=True), machine.id)
get_monitoring_poller().start()
poller = get_monitoring_poller()
poller.start()
poller.kick()
return saved
@@ -68,7 +70,9 @@ def put_machine(
if not store.get_machine(machine_id):
raise HTTPException(status_code=404, detail="Machine not found")
saved = store.upsert_machine(machine.model_dump(exclude_none=True), machine_id)
get_monitoring_poller().start()
poller = get_monitoring_poller()
poller.start()
poller.kick()
return saved
@@ -85,6 +89,8 @@ class SSHKeyInput(BaseModel):
name: str = Field(default="")
private_key: str = Field(default="")
passphrase: str = Field(default="")
public_key: str = Field(default="")
fingerprint: str = Field(default="")
notes: str = Field(default="")
@@ -104,12 +110,14 @@ def generate_ssh_key(
key.write_private_key(private_buffer, password=payload.passphrase or None)
private_key = private_buffer.getvalue()
public_key = f"{key.get_name()} {key.get_base64()}"
fingerprint = ":".join(f"{b:02x}" for b in key.get_fingerprint())
return {
"name": payload.name,
"private_key": private_key,
"passphrase": payload.passphrase,
"notes": payload.notes,
"public_key": public_key,
"fingerprint": fingerprint,
}
@@ -159,6 +159,8 @@ def run_task(
machine_name = _machine_label(machine)
try:
result = client.run(command, timeout=1200)
stdout = result.stdout or ""
stderr = result.stderr or ""
status_text = "success" if result.exit_status == 0 else "error"
store.record_task_run(
task,
@@ -167,9 +169,9 @@ def run_task(
machine_name=machine_name,
task_type=task_type,
duration_ms=int((time.perf_counter() - start) * 1000),
stdout_tail=result.stdout[-4000:],
stderr_tail=result.stderr[-4000:],
error="" if result.exit_status == 0 else (result.stderr or result.stdout or "Task failed"),
stdout_tail=stdout[-4000:],
stderr_tail=stderr[-4000:],
error="" if result.exit_status == 0 else (stderr or stdout or "Task failed"),
)
return {
"task_id": task["id"],
@@ -178,20 +180,31 @@ def run_task(
"machine_name": machine_name,
"task_type": task_type,
"exit_status": result.exit_status,
"stdout": result.stdout,
"stderr": result.stderr,
"stdout": stdout,
"stderr": stderr,
}
except Exception as exc:
duration_ms = int((time.perf_counter() - start) * 1000)
error_text = str(exc)
store.record_task_run(
task,
"error",
machine_id=str(machine.get("id") or ""),
machine_name=machine_name,
task_type=task_type,
duration_ms=int((time.perf_counter() - start) * 1000),
duration_ms=duration_ms,
stdout_tail="",
stderr_tail="",
error=str(exc),
stderr_tail=error_text[-4000:],
error=error_text,
)
logger.exception("Task execution failed task_id=%s", task["id"])
raise
return {
"task_id": task["id"],
"task_name": task["name"],
"machine_id": str(machine.get("id") or ""),
"machine_name": machine_name,
"task_type": task_type,
"exit_status": 1,
"stdout": "",
"stderr": error_text,
}
@@ -64,6 +64,18 @@ class MonitoringPoller:
self._thread.start()
logger.info("Monitoring poller started")
def kick(self) -> None:
"""Run one immediate snapshot cycle in the background."""
store = get_settings_store()
config = self._config()
threading.Thread(
target=self._run_cycle,
args=(store, config),
name="monitoring-poller-kick",
daemon=True,
).start()
logger.info("Monitoring poller kick requested")
def stop(self, timeout: float = 5.0) -> None:
"""Stop the worker thread and wait briefly for shutdown."""
with self._lock:
@@ -79,12 +79,21 @@ class SettingsStore:
name TEXT NOT NULL,
private_key TEXT NOT NULL,
passphrase TEXT NOT NULL,
public_key TEXT NOT NULL DEFAULT '',
fingerprint TEXT NOT NULL DEFAULT '',
notes TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
"""
)
existing_key_columns = {
row[1] for row in conn.execute("PRAGMA table_info(ssh_keys)").fetchall()
}
if "public_key" not in existing_key_columns:
conn.execute("ALTER TABLE ssh_keys ADD COLUMN public_key TEXT NOT NULL DEFAULT ''")
if "fingerprint" not in existing_key_columns:
conn.execute("ALTER TABLE ssh_keys ADD COLUMN fingerprint TEXT NOT NULL DEFAULT ''")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS saved_tasks (
@@ -486,13 +495,15 @@ class SettingsStore:
def _row_to_ssh_key(self, row: sqlite3.Row, usage_count: int = 0) -> dict[str, Any]:
summary = self._private_key_summary(str(row["private_key"] or ""))
public_key = str(row["public_key"] or summary["public_key"] or "")
fingerprint = str(row["fingerprint"] or summary["fingerprint"] or "")
return {
"id": row["id"],
"name": row["name"],
"private_key_set": bool(row["private_key"]),
"passphrase_set": bool(row["passphrase"]),
"public_key": summary["public_key"],
"fingerprint": summary["fingerprint"],
"public_key": public_key,
"fingerprint": fingerprint,
"usage_count": usage_count,
"notes": row["notes"],
"created_at": row["created_at"],
@@ -512,7 +523,10 @@ class SettingsStore:
passphrase = (current or {}).get("passphrase", "")
passphrase = str(passphrase or "")
notes = str(payload.get("notes") if payload.get("notes") is not None else (current or {}).get("notes", "") or "").strip()
return {"id": key_id, "name": name, "private_key": private_key, "passphrase": passphrase, "notes": notes}
summary = self._private_key_summary(private_key)
public_key = str(payload.get("public_key") if payload.get("public_key") is not None else (current or {}).get("public_key", "") or summary["public_key"] or "").strip()
fingerprint = str(payload.get("fingerprint") if payload.get("fingerprint") is not None else (current or {}).get("fingerprint", "") or summary["fingerprint"] or "").strip()
return {"id": key_id, "name": name, "private_key": private_key, "passphrase": passphrase, "public_key": public_key, "fingerprint": fingerprint, "notes": notes}
def list_ssh_keys(self) -> list[dict[str, Any]]:
self.init_schema()
@@ -535,7 +549,7 @@ class SettingsStore:
if not row:
return None
summary = self._private_key_summary(str(row["private_key"] or ""))
return {"id": row["id"], "name": row["name"], "private_key": row["private_key"], "passphrase": row["passphrase"], "notes": row["notes"], "public_key": summary["public_key"], "fingerprint": summary["fingerprint"]}
return {"id": row["id"], "name": row["name"], "private_key": row["private_key"], "passphrase": row["passphrase"], "notes": row["notes"], "public_key": str(row["public_key"] or summary["public_key"] or ""), "fingerprint": str(row["fingerprint"] or summary["fingerprint"] or "")}
def upsert_ssh_key(self, payload: dict[str, Any], key_id: str | None = None) -> dict[str, Any]:
self.init_schema()
@@ -546,16 +560,18 @@ class SettingsStore:
created_at = int(existing[0]) if existing else now
conn.execute(
"""
INSERT INTO ssh_keys (id, name, private_key, passphrase, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO ssh_keys (id, name, private_key, passphrase, public_key, fingerprint, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
private_key = excluded.private_key,
passphrase = excluded.passphrase,
public_key = excluded.public_key,
fingerprint = excluded.fingerprint,
notes = excluded.notes,
updated_at = excluded.updated_at
""",
(key["id"], key["name"], key["private_key"], key["passphrase"], key["notes"], created_at, now),
(key["id"], key["name"], key["private_key"], key["passphrase"], key["public_key"], key["fingerprint"], key["notes"], created_at, now),
)
return self.get_ssh_key(key["id"]) or key
@@ -644,6 +660,46 @@ class SettingsStore:
).fetchall()
return [dict(row) for row in rows]
def record_task_run(
self,
task: dict[str, Any],
status: str,
*,
machine_id: str,
machine_name: str,
task_type: str,
duration_ms: int,
request_id: str = "",
stdout_tail: str = "",
stderr_tail: str = "",
error: str = "",
) -> None:
self.init_schema()
now = int(time.time())
with self.connect() as conn:
conn.execute(
"""
INSERT INTO saved_task_runs
(id, task_id, task_name, machine_id, machine_name, task_type, status, created_at, duration_ms, request_id, stdout_tail, stderr_tail, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
uuid.uuid4().hex,
str(task.get("id") or ""),
str(task.get("name") or ""),
machine_id,
machine_name,
task_type,
status,
now,
duration_ms,
request_id,
stdout_tail,
stderr_tail,
error,
),
)
def _row_to_shortcut(self, row: sqlite3.Row) -> dict[str, Any]:
target = json.loads(row["target_json"] or "{}")
return {