diff --git a/cli/aitbc_cli.py b/cli/aitbc_cli.py index 63d601b9..717a27c8 100644 --- a/cli/aitbc_cli.py +++ b/cli/aitbc_cli.py @@ -1340,11 +1340,11 @@ def main(): network_info = get_network_status(rpc_url=args.rpc_url) if network_info: print("Network Status:") - print(f" Height: {network_info['height']}") - print(f" Latest Block: {network_info['hash'][:16]}...") - print(f" Chain ID: {network_info['chain_id']}") - print(f" RPC Version: {network_info['rpc_version']}") - print(f" Timestamp: {network_info['timestamp']}") + print(f" Height: {network_info.get('height', 'N/A')}") + print(f" Latest Block: {str(network_info.get('hash', 'N/A'))[:16]}...") + print(f" Chain ID: {network_info.get('chain_id', 'ait-mainnet')}") + print(f" Tx Count: {network_info.get('tx_count', 0)}") + print(f" Timestamp: {network_info.get('timestamp', 'N/A')}") else: sys.exit(1) @@ -1507,6 +1507,74 @@ def main(): else: sys.exit(1) + elif args.command == "mine-start": + result = mining_operations('start', wallet=args.wallet) + if result: + print(f"Mining start:") + for key, value in result.items(): + if key != 'action': + print(f" {key.replace('_', ' ').title()}: {value}") + else: + sys.exit(1) + + elif args.command == "mine-stop": + result = mining_operations('stop') + if result: + print(f"Mining stop:") + for key, value in result.items(): + if key != 'action': + print(f" {key.replace('_', ' ').title()}: {value}") + else: + sys.exit(1) + + elif args.command == "mine-status": + result = mining_operations('status') + if result: + print(f"Mining status:") + for key, value in result.items(): + if key != 'action': + print(f" {key.replace('_', ' ').title()}: {value}") + else: + sys.exit(1) + + elif args.command == "market-list": + result = marketplace_operations('list', rpc_url=getattr(args, 'rpc_url', DEFAULT_RPC_URL)) + if result: + print(f"Marketplace listings:") + for key, value in result.items(): + if key != 'action': + if isinstance(value, list): + print(f" {key.replace('_', ' ').title()}:") + for item in value: + print(f" - {item}") + else: + print(f" {key.replace('_', ' ').title()}: {value}") + else: + print("No marketplace listings found.") + + elif args.command == "market-create": + result = marketplace_operations('create', name=getattr(args, 'type', ''), + price=args.price, description=args.description, + wallet=args.wallet) + if result: + print(f"Marketplace listing created:") + for key, value in result.items(): + if key != 'action': + print(f" {key.replace('_', ' ').title()}: {value}") + else: + sys.exit(1) + + elif args.command == "ai-submit": + result = ai_operations('submit', model=getattr(args, 'type', ''), + prompt=args.prompt, wallet=args.wallet) + if result: + print(f"AI job submitted:") + for key, value in result.items(): + if key != 'action': + print(f" {key.replace('_', ' ').title()}: {value}") + else: + sys.exit(1) + else: parser.print_help() diff --git a/dev/scripts/dev_heartbeat.py b/dev/scripts/dev_heartbeat.py index acef3481..f39f79a0 100755 --- a/dev/scripts/dev_heartbeat.py +++ b/dev/scripts/dev_heartbeat.py @@ -13,6 +13,11 @@ from pathlib import Path REPO_ROOT = Path("/opt/aitbc") LOGS_DIR = REPO_ROOT / "logs" +# AITBC blockchain config +LOCAL_RPC = "http://localhost:8006" +GENESIS_RPC = "http://10.1.223.93:8006" +MAX_HEIGHT_DIFF = 10 # acceptable block height difference between nodes + def sh(cmd, cwd=REPO_ROOT): """Run shell command, return (returncode, stdout).""" result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True) @@ -86,18 +91,32 @@ def check_vulnerabilities(): """Run security audits for Python and Node dependencies.""" issues = [] # Python: pip-audit (if available) - rc, out = sh("pip-audit --requirement <(poetry export --without-hashes) 2>&1") - if rc == 0: - # No vulnerabilities - pass + # Export requirements to temp file first to avoid shell process substitution issues + rc_export, req_content = sh("poetry export --without-hashes") + if rc_export == 0 and req_content: + import tempfile + import os + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write(req_content) + temp_req_file = f.name + try: + rc, out = sh(f"pip-audit --requirement {temp_req_file} 2>&1") + if rc == 0: + # No vulnerabilities + pass + else: + # pip-audit returns non-zero when vulns found; parse output for count + # Usually output contains lines with "Found X vulnerabilities" + if "vulnerabilities" in out.lower(): + issues.append(f"Python dependencies: vulnerabilities detected\n```\n{out[:2000]}\n```") + else: + # Command failed for another reason (maybe not installed) + pass + finally: + os.unlink(temp_req_file) else: - # pip-audit returns non-zero when vulns found; parse output for count - # Usually output contains lines with "Found X vulnerabilities" - if "vulnerabilities" in out.lower(): - issues.append(f"Python dependencies: vulnerabilities detected\n```\n{out[:2000]}\n```") - else: - # Command failed for another reason (maybe not installed) - pass + # Failed to export requirements + pass # Node: npm audit (if package.json exists) if (REPO_ROOT / "package.json").exists(): rc, out = sh("npm audit --json") @@ -111,10 +130,79 @@ def check_vulnerabilities(): issues.append("Node dependencies: npm audit failed to parse") return issues +def check_blockchain_health(): + """Check AITBC blockchain node health on this follower node.""" + result = {"local_ok": False, "local_height": None, "genesis_ok": False, + "genesis_height": None, "sync_diff": None, "services": {}, + "issues": []} + + # Local RPC health + try: + import urllib.request + with urllib.request.urlopen(f"{LOCAL_RPC}/rpc/head", timeout=5) as resp: + data = json.loads(resp.read()) + result["local_ok"] = True + result["local_height"] = data.get("height") + except Exception as e: + result["issues"].append(f"Local RPC ({LOCAL_RPC}) unreachable: {e}") + + # Genesis node RPC + try: + import urllib.request + with urllib.request.urlopen(f"{GENESIS_RPC}/rpc/head", timeout=5) as resp: + data = json.loads(resp.read()) + result["genesis_ok"] = True + result["genesis_height"] = data.get("height") + except Exception: + result["issues"].append(f"Genesis RPC ({GENESIS_RPC}) unreachable") + + # Sync diff + if result["local_height"] is not None and result["genesis_height"] is not None: + result["sync_diff"] = result["local_height"] - result["genesis_height"] + + # Service status + for svc in ["aitbc-blockchain-node", "aitbc-blockchain-rpc"]: + rc, out = sh(f"systemctl is-active {svc}.service") + result["services"][svc] = out.strip() if rc == 0 else "unknown" + + return result + + def main(): report = [] issues = 0 + # AITBC Blockchain (always reported) + bc = check_blockchain_health() + bc_lines = [] + bc_issue = False + if bc["local_ok"]: + bc_lines.append(f"- **Follower height**: {bc['local_height']}") + else: + bc_lines.append("- **Follower RPC**: DOWN") + bc_issue = True + if bc["genesis_ok"]: + bc_lines.append(f"- **Genesis height**: {bc['genesis_height']}") + else: + bc_lines.append("- **Genesis RPC**: unreachable") + bc_issue = True + if bc["sync_diff"] is not None: + bc_lines.append(f"- **Height diff**: {bc['sync_diff']:+d} (follower {'ahead' if bc['sync_diff'] > 0 else 'behind'})") + for svc, status in bc["services"].items(): + bc_lines.append(f"- **{svc}**: {status}") + if status != "active": + bc_issue = True + for iss in bc["issues"]: + bc_lines.append(f"- {iss}") + bc_issue = True + if bc_issue: + issues += 1 + report.append("### Blockchain: issues detected\n") + else: + report.append("### Blockchain: healthy\n") + report.extend(bc_lines) + report.append("") + # Git git = check_git_status() if git and git["changed"] > 0: