Add node deletion functionality to FARMQ Admin
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
11f6ff16d0
commit
45c952258b
@ -15,6 +15,7 @@ from utils.database_new import (
|
|||||||
get_machine_detail, get_pharmacy_detail, get_active_alerts,
|
get_machine_detail, get_pharmacy_detail, get_active_alerts,
|
||||||
sync_machines_from_headscale, sync_users_from_headscale
|
sync_machines_from_headscale, sync_users_from_headscale
|
||||||
)
|
)
|
||||||
|
import subprocess
|
||||||
from utils.proxmox_client import ProxmoxClient
|
from utils.proxmox_client import ProxmoxClient
|
||||||
|
|
||||||
def create_app(config_name=None):
|
def create_app(config_name=None):
|
||||||
@ -400,6 +401,42 @@ def create_app(config_name=None):
|
|||||||
print(f"❌ VM 정지 오류: {e}")
|
print(f"❌ VM 정지 오류: {e}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
# 노드 삭제 API
|
||||||
|
@app.route('/api/nodes/<int:node_id>/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)
|
@app.errorhandler(404)
|
||||||
def not_found_error(error):
|
def not_found_error(error):
|
||||||
|
|||||||
@ -189,6 +189,11 @@
|
|||||||
<i class="fas fa-redo"></i>
|
<i class="fas fa-redo"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<button class="btn btn-outline-danger"
|
||||||
|
onclick="confirmDeleteNode({{ machine_data.id }}, '{{ machine_data.machine_name or machine_data.hostname }}')"
|
||||||
|
title="노드 삭제">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -298,6 +303,10 @@
|
|||||||
onclick="showMonitoring({{ machine_data.id }})">
|
onclick="showMonitoring({{ machine_data.id }})">
|
||||||
<i class="fas fa-chart-line"></i> 모니터링
|
<i class="fas fa-chart-line"></i> 모니터링
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-outline-danger btn-sm"
|
||||||
|
onclick="confirmDeleteNode({{ machine_data.id }}, '{{ machine_data.machine_name or machine_data.hostname }}')">
|
||||||
|
<i class="fas fa-trash"></i> 삭제
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -396,6 +405,41 @@ function refreshMachineList() {
|
|||||||
}, 1000);
|
}, 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() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
filterMachines();
|
filterMachines();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user