#!/usr/bin/env python3 """kuma-regroup.py — move Uptime Kuma monitors into groups [#435] Founder ruling 2026-09-02: no monitors at the root; use the existing groups. Uses the uptime-kuma-api library — raw socket.io editMonitor with a hand-built payload times out on this Kuma build (missing form fields the server requires before acking; the lib sends the full UI-shaped payload). dns_resolve_type="A" is passed to satisfy lib-side validation for monitors where the field is None (harmless for non-dns types). Env (from ~/.creds/uptime-kuma.env): UPTIME_KUMA_URL, UPTIME_KUMA_TOKEN Usage (host — needs python-socketio-compatible env via lib): python3 kuma-regroup.py --dry-move 211:48 213:13 python3 kuma-regroup.py --move 211:48 213:13 id:groupid pairs. --move executes; --dry-move prints the plan only. Verify afterwards with kuma-inventory.py (the lib's get_monitors does not map the parent field — trust the raw monitorList view). """ import argparse import os import sys from uptime_kuma_api import UptimeKumaApi def main(): ap = argparse.ArgumentParser() ap.add_argument("--move", nargs="+", default=[], metavar="ID:GROUPID") ap.add_argument("--dry-move", nargs="+", default=[], metavar="ID:GROUPID") args = ap.parse_args() pairs = [(int(a), int(b)) for a, b in (p.split(":") for p in (args.move or args.dry_move))] dry = bool(args.dry_move) api = UptimeKumaApi(os.environ["UPTIME_KUMA_URL"], timeout=30) rc = 0 try: api.login_by_token(os.environ["UPTIME_KUMA_TOKEN"]) monitors = {m["id"]: m for m in api.get_monitors()} for mid, gid in pairs: m = monitors.get(mid) if not m: print(f"SKIP {mid}: not found") rc = 1 continue g = monitors.get(gid) gname = g["name"] if g else "?" if dry: print(f"DRY {mid} '{m['name']}' -> group {gid} '{gname}'") continue try: r = api.edit_monitor(mid, parent=gid, dns_resolve_type="A") ok = isinstance(r, dict) and ("Saved" in str(r.get("msg", ""))) print(f"MOVE {mid} '{m['name']}' -> {gid} '{gname}': {r.get('msg') if ok else r}") if not ok: rc = 1 except Exception as e: # noqa: BLE001 — per-move isolation, batch continues print(f"MOVE {mid} FAIL: {str(e)[:120]}") rc = 1 finally: api.disconnect() return rc if __name__ == "__main__": sys.exit(main())