From 45c952258b1512f7271a0c85d94875bcc856cd29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Thu, 11 Sep 2025 10:44:49 +0900 Subject: [PATCH] Add node deletion functionality to FARMQ Admin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DELETE API endpoint for node deletion via Headscale CLI - Add delete buttons to both table and card views in machine list - Implement confirmation dialog with clear warning message - Add proper error handling and user feedback with toast messages - Auto-refresh page after successful deletion - Match Headplane functionality for complete node management ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- farmq-admin/app.py | 37 ++++++++++++++++++++ farmq-admin/templates/machines/list.html | 44 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/farmq-admin/app.py b/farmq-admin/app.py index a217a91..c232a88 100644 --- a/farmq-admin/app.py +++ b/farmq-admin/app.py @@ -15,6 +15,7 @@ from utils.database_new import ( get_machine_detail, get_pharmacy_detail, get_active_alerts, sync_machines_from_headscale, sync_users_from_headscale ) +import subprocess from utils.proxmox_client import ProxmoxClient def create_app(config_name=None): @@ -400,6 +401,42 @@ def create_app(config_name=None): print(f"โŒ VM ์ •์ง€ ์˜ค๋ฅ˜: {e}") return jsonify({'error': str(e)}), 500 + # ๋…ธ๋“œ ์‚ญ์ œ API + @app.route('/api/nodes//delete', methods=['DELETE']) + def api_delete_node(node_id): + """๋…ธ๋“œ ์‚ญ์ œ API""" + try: + # Headscale CLI๋ฅผ ํ†ตํ•ด ๋…ธ๋“œ ์‚ญ์ œ + result = subprocess.run( + ['docker', 'exec', 'headscale', 'headscale', 'nodes', 'delete', '-i', str(node_id), '--force'], + capture_output=True, + text=True, + check=True + ) + + # ์‚ญ์ œ ์„ฑ๊ณต + return jsonify({ + 'success': True, + 'message': f'๋…ธ๋“œ {node_id}๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.', + 'output': result.stdout + }) + + except subprocess.CalledProcessError as e: + # Headscale CLI ์˜ค๋ฅ˜ + error_msg = e.stderr if e.stderr else e.stdout + return jsonify({ + 'success': False, + 'error': f'๋…ธ๋“œ ์‚ญ์ œ ์‹คํŒจ: {error_msg}' + }), 400 + + except Exception as e: + # ๊ธฐํƒ€ ์˜ค๋ฅ˜ + print(f"โŒ ๋…ธ๋“œ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}") + return jsonify({ + 'success': False, + 'error': f'์„œ๋ฒ„ ์˜ค๋ฅ˜: {str(e)}' + }), 500 + # ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ @app.errorhandler(404) def not_found_error(error): diff --git a/farmq-admin/templates/machines/list.html b/farmq-admin/templates/machines/list.html index bb646a5..e64c4aa 100644 --- a/farmq-admin/templates/machines/list.html +++ b/farmq-admin/templates/machines/list.html @@ -189,6 +189,11 @@ {% endif %} + @@ -298,6 +303,10 @@ onclick="showMonitoring({{ machine_data.id }})"> ๋ชจ๋‹ˆํ„ฐ๋ง + @@ -396,6 +405,41 @@ function refreshMachineList() { }, 1000); } +// ๋…ธ๋“œ ์‚ญ์ œ ํ™•์ธ +function confirmDeleteNode(nodeId, nodeName) { + if (confirm(`์ •๋ง๋กœ ๋…ธ๋“œ "${nodeName}"๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?\n\n์‚ญ์ œ๋œ ๋…ธ๋“œ๋Š” ๋ณต๊ตฌํ•  ์ˆ˜ ์—†์œผ๋ฉฐ, ํ•ด๋‹น ๋จธ์‹ ์€ ๋„คํŠธ์›Œํฌ์—์„œ ์™„์ „ํžˆ ์ œ๊ฑฐ๋ฉ๋‹ˆ๋‹ค.`)) { + deleteNode(nodeId, nodeName); + } +} + +// ๋…ธ๋“œ ์‚ญ์ œ ์‹คํ–‰ +function deleteNode(nodeId, nodeName) { + showToast(`๋…ธ๋“œ ${nodeName} ์‚ญ์ œ ์ค‘...`, 'info'); + + fetch(`/api/nodes/${nodeId}/delete`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(`๋…ธ๋“œ ${nodeName}๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, 'success'); + // ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ์œผ๋กœ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ + setTimeout(() => { + location.reload(); + }, 1500); + } else { + showToast(`๋…ธ๋“œ ์‚ญ์ œ ์‹คํŒจ: ${data.error}`, 'danger'); + } + }) + .catch(error => { + console.error('๋…ธ๋“œ ์‚ญ์ œ ์˜ค๋ฅ˜:', error); + showToast('๋…ธ๋“œ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.', 'danger'); + }); +} + // ์ดˆ๊ธฐ ์นด์šดํ„ฐ ์„ค์ • document.addEventListener('DOMContentLoaded', function() { filterMachines();