Implement Headscale machine rename functionality in FarmQ Admin
Add machine name editing feature similar to Headplane:
- REST API endpoint POST /api/machines/{id}/rename with Headscale CLI integration
- Edit button next to each machine name in the machine list
- Modal dialog with real-time Magic DNS preview
- DNS-compatible name validation (lowercase, digits, hyphens only)
- Immediate UI updates after successful rename
- Loading states and error handling
Features:
- farmq-admin/app.py: New rename API endpoint with subprocess Headscale CLI calls
- farmq-admin/templates/machines/list.html: Edit buttons, rename modal, JavaScript functions
- Real-time validation and Magic DNS preview
- Bootstrap modal with form validation
- Error handling with toast notifications
Users can now rename machines and their Magic DNS addresses directly from the web interface,
matching the functionality available in Headplane.
Tested with machine ID 2: desktop-emjd1dc ↔ desktop-emjd1dc-test ✅
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -112,9 +112,16 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ machine_data.machine_name or machine_data.hostname }}</strong>
|
||||
<div class="d-flex align-items-center">
|
||||
<strong id="machineName-{{ machine_data.id }}">{{ machine_data.machine_name or machine_data.hostname }}</strong>
|
||||
<button class="btn btn-sm btn-outline-primary ms-2"
|
||||
onclick="showRenameModal({{ machine_data.id }}, '{{ machine_data.machine_name or machine_data.hostname }}')"
|
||||
title="머신 이름 변경">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="small text-success">
|
||||
<i class="fas fa-link"></i> <code>{{ machine_data.machine_name or machine_data.hostname }}.headscale.local</code>
|
||||
<i class="fas fa-link"></i> <code id="magicDns-{{ machine_data.id }}">{{ machine_data.machine_name or machine_data.hostname }}.headscale.local</code>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-1" onclick="copyToClipboard('{{ machine_data.machine_name or machine_data.hostname }}.headscale.local')" title="Magic DNS 주소 복사">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
@@ -460,9 +467,137 @@ function copyToClipboard(text) {
|
||||
});
|
||||
}
|
||||
|
||||
// 머신 이름 변경 모달 표시
|
||||
function showRenameModal(machineId, currentName) {
|
||||
document.getElementById('renameModal').dataset.machineId = machineId;
|
||||
document.getElementById('currentMachineName').textContent = currentName;
|
||||
document.getElementById('newMachineName').value = currentName;
|
||||
|
||||
// 모달 표시
|
||||
const modal = new bootstrap.Modal(document.getElementById('renameModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// 머신 이름 변경 실행
|
||||
function renameMachine() {
|
||||
const modal = document.getElementById('renameModal');
|
||||
const machineId = modal.dataset.machineId;
|
||||
const newName = document.getElementById('newMachineName').value.trim();
|
||||
|
||||
if (!newName) {
|
||||
showToast('새로운 이름을 입력해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 이름 유효성 검사
|
||||
const namePattern = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
||||
if (!namePattern.test(newName)) {
|
||||
showToast('이름은 소문자, 숫자, 하이픈(-)만 사용 가능하며, 하이픈으로 시작하거나 끝날 수 없습니다.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 로딩 상태 표시
|
||||
const submitBtn = document.getElementById('renameSubmitBtn');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 변경 중...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// API 호출
|
||||
fetch(`/api/machines/${machineId}/rename`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ new_name: newName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
|
||||
// UI 업데이트
|
||||
document.getElementById(`machineName-${machineId}`).textContent = data.new_name;
|
||||
document.getElementById(`magicDns-${machineId}`).textContent = data.new_magic_dns;
|
||||
|
||||
// 모달 닫기
|
||||
bootstrap.Modal.getInstance(document.getElementById('renameModal')).hide();
|
||||
} else {
|
||||
showToast(data.error || '이름 변경에 실패했습니다.', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('머신 이름 변경 오류:', error);
|
||||
showToast('서버 오류가 발생했습니다.', 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
// 로딩 상태 해제
|
||||
submitBtn.innerHTML = originalText;
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 초기 카운터 설정
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
filterMachines();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 머신 이름 변경 모달 -->
|
||||
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="renameModalLabel">
|
||||
<i class="fas fa-edit"></i> 머신 이름 변경
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">현재 이름</label>
|
||||
<div class="form-control-plaintext">
|
||||
<strong id="currentMachineName"></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newMachineName" class="form-label">새로운 이름</label>
|
||||
<input type="text" class="form-control" id="newMachineName"
|
||||
placeholder="새로운 머신 이름을 입력하세요"
|
||||
pattern="^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
|
||||
oninput="updatePreview()">
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
소문자, 숫자, 하이픈(-)만 사용 가능하며, 하이픈으로 시작하거나 끝날 수 없습니다.
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-magic"></i>
|
||||
<strong>Magic DNS 주소:</strong>
|
||||
<span id="previewMagicDns">새이름.headscale.local</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-primary" id="renameSubmitBtn" onclick="renameMachine()">
|
||||
<i class="fas fa-save"></i> 변경
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Magic DNS 미리보기 업데이트
|
||||
function updatePreview() {
|
||||
const newName = document.getElementById('newMachineName').value.trim();
|
||||
const preview = document.getElementById('previewMagicDns');
|
||||
|
||||
if (newName) {
|
||||
preview.textContent = `${newName}.headscale.local`;
|
||||
} else {
|
||||
preview.textContent = '새이름.headscale.local';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user