steps for launching

This commit is contained in:
2025-06-12 15:40:57 +02:00
parent 83c94acbac
commit a801eb1eeb
5 changed files with 513 additions and 227 deletions

View File

@@ -35,7 +35,7 @@
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>PORT</th>
<th>Company</th>
<th>Rooms</th>
<th>Conversations</th>
@@ -156,18 +156,22 @@
</div>
<div class="step-item" data-step="2">
<div class="step-circle">2</div>
<div class="step-label">Configuration</div>
<div class="step-label">Repository</div>
</div>
<div class="step-item" data-step="3">
<div class="step-circle">3</div>
<div class="step-label">Resources</div>
<div class="step-label">Company</div>
</div>
<div class="step-item" data-step="4">
<div class="step-circle">4</div>
<div class="step-label">Review</div>
<div class="step-label">Variables</div>
</div>
<div class="step-item" data-step="5">
<div class="step-circle">5</div>
<div class="step-label">White Label</div>
</div>
<div class="step-item" data-step="6">
<div class="step-circle">6</div>
<div class="step-label">Launch</div>
</div>
</div>
@@ -176,9 +180,6 @@
<div class="step-content">
<!-- Step 1 -->
<div class="step-pane active" id="step1">
<h4>Connection Verification</h4>
<p class="text-muted mb-4">Let's verify that your connections are properly configured before proceeding.</p>
<!-- Portainer Connection -->
<div class="connection-check mb-4">
<div class="d-flex align-items-center mb-2">
@@ -223,24 +224,149 @@
<!-- Step 2 -->
<div class="step-pane" id="step2">
<h4>Configuration</h4>
<p class="text-muted">Configure your instance settings.</p>
<div class="step-content" id="step2">
<div class="card">
<div class="card-body">
<div class="mb-3">
<label class="form-label">Repository</label>
<select class="form-select" id="giteaRepoSelect">
<option value="">Loading repositories...</option>
</select>
<div class="form-text">Select the repository containing your application code</div>
</div>
<div class="mb-3">
<label class="form-label">Branch</label>
<select class="form-select" id="giteaBranchSelect" disabled>
<option value="">Select a repository first</option>
</select>
<div class="form-text">Select the branch to deploy</div>
</div>
</div>
</div>
</div>
</div>
<!-- Step 3 -->
<div class="step-pane" id="step3">
<h4>Resources</h4>
<p class="text-muted">Set up your instance resources.</p>
<div class="step-content">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Company Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="companyName" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Industry</label>
<input type="text" class="form-control" id="industry">
</div>
</div>
<div class="mb-3">
<label class="form-label">Street Address <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="streetAddress" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">City <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="city" required>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">State</label>
<input type="text" class="form-control" id="state">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">ZIP Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="zipCode" required>
</div>
</div>
<div class="mb-3">
<label class="form-label">Country <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="country" required>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">Website</label>
<input type="url" class="form-control" id="website">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email" class="form-control" id="email" required>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Phone <span class="text-danger">*</span></label>
<input type="tel" class="form-control" id="phone" required>
</div>
</div>
<div class="mb-3">
<label class="form-label">Company Description</label>
<textarea class="form-control" id="companyDescription" rows="4"></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Step 4 -->
<div class="step-pane" id="step4">
<h4>Review</h4>
<p class="text-muted">Review your instance configuration.</p>
<h5 class="mb-4">Port Configuration</h5>
<div class="mb-3">
<label for="port" class="form-label">Port Number <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="port" required min="1024" max="65535">
<div class="form-text">Ports below 1024 are reserved for system use. Suggested port: <span id="suggestedPort">3000</span></div>
</div>
</div>
<!-- Step 5 -->
<!-- Step 5: White Label -->
<div class="step-pane" id="step5">
<div class="step-content">
<div class="card">
<div class="card-body">
<h5 class="card-title mb-4">White Label Configuration</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Primary Color</label>
<div class="input-group">
<input type="color" class="form-control form-control-color" id="primaryColor" value="#16767B" title="Choose primary color">
<input type="text" class="form-control" id="primaryColorRGB" value="22,118,123" readonly>
</div>
<div class="form-text">Default: RGB(22,118,123)</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Secondary Color</label>
<div class="input-group">
<input type="color" class="form-control form-control-color" id="secondaryColor" value="#741B5F" title="Choose secondary color">
<input type="text" class="form-control" id="secondaryColorRGB" value="116,27,95" readonly>
</div>
<div class="form-text">Default: RGB(116,27,95)</div>
</div>
</div>
</div>
<div class="mt-4">
<h6>Preview</h6>
<div id="previewContainer">
<div class="preview-box p-3 rounded mb-2" id="primaryPreview" style="background-color: #16767B; color: white;">
Primary Color Sample
</div>
<div class="preview-box p-3 rounded" id="secondaryPreview" style="background-color: #741B5F; color: white;">
Secondary Color Sample
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Step 6 -->
<div class="step-pane" id="step6">
<h4>Launch</h4>
<p class="text-muted">Ready to launch your instance!</p>
</div>
@@ -448,6 +574,9 @@ let authModal;
let launchStepsModal;
let currentStep = 1;
// Update the total number of steps
const totalSteps = 6;
document.addEventListener('DOMContentLoaded', function() {
addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal'));
editInstanceModal = new bootstrap.Modal(document.getElementById('editInstanceModal'));
@@ -484,6 +613,47 @@ document.addEventListener('DOMContentLoaded', function() {
// Set up periodic status checks (every 30 seconds)
setInterval(checkAllInstanceStatuses, 30000);
// Update color picker functionality
const primaryColor = document.getElementById('primaryColor');
const secondaryColor = document.getElementById('secondaryColor');
const primaryColorRGB = document.getElementById('primaryColorRGB');
const secondaryColorRGB = document.getElementById('secondaryColorRGB');
const primaryPreview = document.getElementById('primaryPreview');
const secondaryPreview = document.getElementById('secondaryPreview');
function updateColorPreview() {
// Update preview boxes directly instead of using CSS variables
primaryPreview.style.backgroundColor = primaryColor.value;
secondaryPreview.style.backgroundColor = secondaryColor.value;
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ?
`${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` :
null;
}
function rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('');
}
primaryColor.addEventListener('input', function() {
primaryColorRGB.value = hexToRgb(this.value);
updateColorPreview();
});
secondaryColor.addEventListener('input', function() {
secondaryColorRGB.value = hexToRgb(this.value);
updateColorPreview();
});
// Initialize color preview
updateColorPreview();
});
// Function to check status of all instances
@@ -1105,51 +1275,48 @@ function nextStepLaunchInstance() {
}
function updateStepDisplay() {
// Update step indicators
document.querySelectorAll('.step-item').forEach((item, index) => {
const stepNum = index + 1;
item.classList.remove('active', 'completed');
if (stepNum === currentStep) {
item.classList.add('active');
} else if (stepNum < currentStep) {
item.classList.add('completed');
}
// Hide all steps
document.querySelectorAll('.step-pane').forEach(pane => {
pane.classList.remove('active');
});
// Update step content
document.querySelectorAll('.step-pane').forEach((pane, index) => {
pane.classList.remove('active');
if (index + 1 === currentStep) {
pane.classList.add('active');
}
// Show current step
document.getElementById(`step${currentStep}`).classList.add('active');
// Update step indicators
document.querySelectorAll('.step-item').forEach(item => {
const step = parseInt(item.getAttribute('data-step'));
item.classList.toggle('active', step === currentStep);
item.classList.toggle('completed', step < currentStep);
});
// Update buttons
const prevButton = document.querySelector('#launchStepsModal .btn-secondary');
const nextButton = document.querySelector('#launchStepsModal .btn-primary');
const prevButton = document.querySelector('.modal-footer .btn-secondary');
const nextButton = document.querySelector('.modal-footer .btn-primary');
prevButton.style.display = currentStep === 1 ? 'none' : 'inline-block';
nextButton.innerHTML = currentStep === 5 ? 'Launch <i class="fas fa-rocket ms-1"></i>' : 'Next <i class="fas fa-arrow-right ms-1"></i>';
nextButton.innerHTML = currentStep === totalSteps ? 'Launch Instance' : 'Next <i class="fas fa-arrow-right ms-1"></i>';
// If we're on step 4, get the next available port
if (currentStep === 4) {
getNextAvailablePort();
}
}
function nextStep() {
if (currentStep < 5) {
// If we're on step 1, verify connections before proceeding
if (currentStep === 1) {
const portainerStatus = document.getElementById('portainerStatus');
const nginxStatus = document.getElementById('nginxStatus');
if (!portainerStatus.innerHTML.includes('Connected') || !nginxStatus.innerHTML.includes('Connected')) {
alert('Please ensure all connections are working before proceeding.');
return;
}
}
if (currentStep === 2 && !validateStep2()) {
return;
}
if (currentStep === 3 && !validateStep3()) {
return;
}
if (currentStep < totalSteps) {
currentStep++;
updateStepDisplay();
} else {
// Handle launch
console.log('Launching instance...');
// Handle final step (launch instance)
launchInstance();
}
}
@@ -1232,5 +1399,226 @@ async function verifyConnections() {
nginxDetails.innerHTML = `<small class="text-danger">${error.message || 'Error checking NGINX connection'}</small>`;
}
}
// Repository Selection Functions
async function loadRepositories() {
const repoSelect = document.getElementById('giteaRepoSelect');
const branchSelect = document.getElementById('giteaBranchSelect');
try {
const gitSettings = JSON.parse('{{ git_settings|tojson|safe }}');
if (!gitSettings || !gitSettings.url || !gitSettings.token) {
throw new Error('No Git settings found. Please configure Git in the settings page.');
}
// Load repositories using the correct endpoint
const repoResponse = await fetch('/api/admin/list-gitea-repos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
url: gitSettings.url,
token: gitSettings.token
})
});
if (!repoResponse.ok) {
throw new Error('Failed to load repositories');
}
const data = await repoResponse.json();
if (data.repositories) {
repoSelect.innerHTML = '<option value="">Select a repository</option>' +
data.repositories.map(repo =>
`<option value="${repo.full_name}" ${repo.full_name === gitSettings.repo ? 'selected' : ''}>${repo.full_name}</option>`
).join('');
repoSelect.disabled = false;
// If we have a saved repository, load its branches
if (gitSettings.repo) {
loadBranches(gitSettings.repo);
}
} else {
repoSelect.innerHTML = '<option value="">No repositories found</option>';
repoSelect.disabled = true;
}
} catch (error) {
console.error('Error loading repositories:', error);
repoSelect.innerHTML = `<option value="">Error: ${error.message}</option>`;
repoSelect.disabled = true;
}
}
async function loadBranches(repoId) {
const branchSelect = document.getElementById('giteaBranchSelect');
try {
const gitSettings = JSON.parse('{{ git_settings|tojson|safe }}');
if (!gitSettings || !gitSettings.url || !gitSettings.token) {
throw new Error('No Git settings found. Please configure Git in the settings page.');
}
const response = await fetch('/api/admin/list-gitea-branches', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
url: gitSettings.url,
token: gitSettings.token,
repo: repoId
})
});
if (!response.ok) {
throw new Error('Failed to load branches');
}
const data = await response.json();
if (data.branches) {
branchSelect.innerHTML = '<option value="">Select a branch</option>' +
data.branches.map(branch =>
`<option value="${branch.name}" ${branch.name === 'master' ? 'selected' : ''}>${branch.name}</option>`
).join('');
branchSelect.disabled = false;
} else {
branchSelect.innerHTML = '<option value="">No branches found</option>';
branchSelect.disabled = true;
}
} catch (error) {
console.error('Error loading branches:', error);
branchSelect.innerHTML = `<option value="">Error: ${error.message}</option>`;
branchSelect.disabled = true;
}
}
// Event Listeners for Repository Selection
document.getElementById('giteaRepoSelect').addEventListener('change', function() {
const repoId = this.value;
if (repoId) {
loadBranches(repoId);
} else {
document.getElementById('giteaBranchSelect').innerHTML = '<option value="">Select a repository first</option>';
document.getElementById('giteaBranchSelect').disabled = true;
}
});
// Load repositories when step 2 is shown
document.addEventListener('DOMContentLoaded', function() {
const step2Content = document.getElementById('step2');
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.target.classList.contains('active')) {
loadRepositories();
}
});
});
observer.observe(step2Content, { attributes: true, attributeFilter: ['class'] });
});
// Function to get the next available port
async function getNextAvailablePort() {
try {
// Get all existing instances
const response = await fetch('/instances');
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// Get all port numbers from the table
const portCells = doc.querySelectorAll('table tbody tr td:first-child');
const ports = Array.from(portCells)
.map(cell => {
const portText = cell.textContent.trim();
// Only parse if it's a number
return /^\d+$/.test(portText) ? parseInt(portText) : null;
})
.filter(port => port !== null)
.sort((a, b) => a - b); // Sort numerically
console.log('Found existing ports:', ports);
// Find the next available port
let nextPort = 10339; // Start from the next port after your highest
while (ports.includes(nextPort)) {
nextPort++;
}
console.log('Next available port:', nextPort);
// Set the suggested port
document.getElementById('port').value = nextPort;
} catch (error) {
console.error('Error getting next available port:', error);
// Default to 10339 if there's an error
document.getElementById('port').value = 10339;
}
}
// Function to validate step 2
function validateStep2() {
const repositorySelect = document.getElementById('giteaRepoSelect');
const branchSelect = document.getElementById('giteaBranchSelect');
if (!repositorySelect.value) {
alert('Please select a repository');
return false;
}
if (!branchSelect.value) {
alert('Please select a branch');
return false;
}
return true;
}
// Function to validate step 3
function validateStep3() {
const requiredFields = [
{ id: 'companyName', name: 'Company Name' },
{ id: 'streetAddress', name: 'Street Address' },
{ id: 'city', name: 'City' },
{ id: 'zipCode', name: 'ZIP Code' },
{ id: 'country', name: 'Country' },
{ id: 'email', name: 'Email' },
{ id: 'phone', name: 'Phone' }
];
const missingFields = requiredFields.filter(field => {
const element = document.getElementById(field.id);
return !element.value.trim();
});
if (missingFields.length > 0) {
const fieldNames = missingFields.map(field => field.name).join(', ');
alert(`Please fill in all required fields: ${fieldNames}`);
return false;
}
// Validate email format
const email = document.getElementById('email').value;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
alert('Please enter a valid email address');
return false;
}
// Validate phone format (basic validation)
const phone = document.getElementById('phone').value;
const phoneRegex = /^[\d\s\-\+\(\)]{10,}$/;
if (!phoneRegex.test(phone)) {
alert('Please enter a valid phone number');
return false;
}
return true;
}
</script>
{% endblock %}