139 lines
4.7 KiB
Twig
139 lines
4.7 KiB
Twig
{% extends "layout.twig" %}
|
|
{% block title %}Deploy {{ server.name }}{% endblock %}
|
|
{% block content %}
|
|
<div class="max-w-4xl mx-auto px-4 py-8">
|
|
<h1 class="text-2xl font-bold mb-1">Deploying: {{ server.name }}</h1>
|
|
<p class="text-sm text-gray-400 mb-6">Protocol: {{ server.install_protocol ?? 'amnezia-wg' }}</p>
|
|
<div id="deployLog" class="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm h-96 overflow-y-auto mb-4">
|
|
<div>Ready to deploy...</div>
|
|
</div>
|
|
<div id="deployActions" class="flex gap-3 mb-4 hidden"></div>
|
|
<button id="deployBtn" onclick="deploy()" class="gradient-bg text-white px-6 py-2 rounded hover:opacity-90 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<span id="btnText">Start Deployment</span>
|
|
<i id="btnSpinner" class="fas fa-spinner fa-spin ml-2 hidden"></i>
|
|
</button>
|
|
</div>
|
|
<script>
|
|
let pendingDecisionToken = null;
|
|
|
|
function setButtonState(isProcessing, label) {
|
|
const btn = document.getElementById('deployBtn');
|
|
const btnText = document.getElementById('btnText');
|
|
const btnSpinner = document.getElementById('btnSpinner');
|
|
btn.disabled = isProcessing;
|
|
btnText.textContent = label;
|
|
if (isProcessing) {
|
|
btnSpinner.classList.remove('hidden');
|
|
} else {
|
|
btnSpinner.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function appendLog(message, cssClass) {
|
|
const log = document.getElementById('deployLog');
|
|
const line = document.createElement('div');
|
|
if (cssClass) {
|
|
line.className = cssClass;
|
|
}
|
|
line.innerHTML = message;
|
|
log.appendChild(line);
|
|
log.scrollTop = log.scrollHeight;
|
|
}
|
|
|
|
function hideActions() {
|
|
const actions = document.getElementById('deployActions');
|
|
actions.classList.add('hidden');
|
|
actions.innerHTML = '';
|
|
}
|
|
|
|
function showActions(options) {
|
|
const actions = document.getElementById('deployActions');
|
|
actions.innerHTML = '';
|
|
Object.keys(options || {}).forEach(key => {
|
|
const option = options[key];
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.textContent = option.label || key;
|
|
btn.className = 'px-4 py-2 rounded bg-white text-gray-800 border border-gray-200 shadow-sm hover:bg-gray-50 transition';
|
|
btn.onclick = function () {
|
|
hideActions();
|
|
deploy(option.mode || key);
|
|
};
|
|
actions.appendChild(btn);
|
|
});
|
|
if (actions.childElementCount > 0) {
|
|
actions.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function deploy(mode) {
|
|
const payload = {};
|
|
if (mode) {
|
|
payload.install_mode = mode;
|
|
}
|
|
if (pendingDecisionToken) {
|
|
payload.decision_token = pendingDecisionToken;
|
|
}
|
|
|
|
if (!mode) {
|
|
pendingDecisionToken = null;
|
|
document.getElementById('deployLog').innerHTML = '';
|
|
appendLog('📡 Connecting to server...');
|
|
appendLog('🔧 Preparing environment...');
|
|
}
|
|
|
|
hideActions();
|
|
setButtonState(true, mode ? 'Processing...' : 'Deploying...');
|
|
|
|
fetch('/servers/{{ server.id }}/deploy', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(async response => {
|
|
const data = await response.json().catch(() => ({ success: false, error: 'Invalid server response' }));
|
|
if (!response.ok && !data.requires_action) {
|
|
throw new Error(data.error || ('HTTP ' + response.status));
|
|
}
|
|
return data;
|
|
})
|
|
.then(data => {
|
|
if (data.requires_action) {
|
|
pendingDecisionToken = data.decision_token || null;
|
|
const details = data.details || {};
|
|
appendLog('⚠️ ' + (details.message || 'Existing configuration detected'), 'text-yellow-300');
|
|
if (details.details && details.details.summary) {
|
|
appendLog(details.details.summary, 'text-yellow-200');
|
|
} else if (details.details) {
|
|
appendLog(JSON.stringify(details.details), 'text-yellow-200 text-xs');
|
|
}
|
|
showActions(data.options);
|
|
setButtonState(false, 'Select action');
|
|
return;
|
|
}
|
|
|
|
if (data.success) {
|
|
pendingDecisionToken = null;
|
|
hideActions();
|
|
appendLog('✅ Deployment successful!', 'text-green-500 font-bold');
|
|
if (data.vpn_port) {
|
|
appendLog('🔌 VPN Port: ' + data.vpn_port, 'text-yellow-300');
|
|
}
|
|
if (data.public_key) {
|
|
appendLog('🔑 Public Key: ' + data.public_key.substring(0, 40) + '...', 'text-yellow-300');
|
|
}
|
|
setButtonState(true, 'Redirecting...');
|
|
setTimeout(() => window.location.href = '/servers/{{ server.id }}', 2000);
|
|
} else {
|
|
appendLog('❌ ' + (data.error || 'Unknown error'), 'text-red-500 font-bold');
|
|
setButtonState(false, 'Retry Deployment');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
appendLog('❌ Network error: ' + error.message, 'text-red-500 font-bold');
|
|
setButtonState(false, 'Retry Deployment');
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|