Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 912f97490c | |||
| d7f5809771 |
Binary file not shown.
Binary file not shown.
@@ -675,4 +675,54 @@ def update_pricing_plan_status(plan_id):
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@admin.route('/api/admin/pricing-plans', methods=['GET'])
|
||||
@login_required
|
||||
def get_pricing_plans():
|
||||
"""Get all active pricing plans for instance launch"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
try:
|
||||
from models import PricingPlan
|
||||
|
||||
# Get all active pricing plans ordered by order_index
|
||||
plans = PricingPlan.query.filter_by(is_active=True).order_by(PricingPlan.order_index).all()
|
||||
|
||||
plans_data = []
|
||||
for plan in plans:
|
||||
plans_data.append({
|
||||
'id': plan.id,
|
||||
'name': plan.name,
|
||||
'description': plan.description,
|
||||
'monthly_price': plan.monthly_price,
|
||||
'annual_price': plan.annual_price,
|
||||
'features': plan.features,
|
||||
'button_text': plan.button_text,
|
||||
'button_url': plan.button_url,
|
||||
'is_popular': plan.is_popular,
|
||||
'is_custom': plan.is_custom,
|
||||
'is_active': plan.is_active,
|
||||
'order_index': plan.order_index,
|
||||
'room_quota': plan.room_quota,
|
||||
'conversation_quota': plan.conversation_quota,
|
||||
'storage_quota_gb': plan.storage_quota_gb,
|
||||
'manager_quota': plan.manager_quota,
|
||||
'admin_quota': plan.admin_quota,
|
||||
'format_quota_display': {
|
||||
'room_quota': plan.format_quota_display('room_quota'),
|
||||
'conversation_quota': plan.format_quota_display('conversation_quota'),
|
||||
'storage_quota_gb': plan.format_quota_display('storage_quota_gb'),
|
||||
'manager_quota': plan.format_quota_display('manager_quota'),
|
||||
'admin_quota': plan.format_quota_display('admin_quota')
|
||||
}
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'plans': plans_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -580,7 +580,8 @@ def get_version_info(current_user):
|
||||
'git_branch': os.environ.get('GIT_BRANCH', 'unknown'),
|
||||
'deployed_at': os.environ.get('DEPLOYED_AT', 'unknown'),
|
||||
'ismaster': os.environ.get('ISMASTER', 'false'),
|
||||
'port': os.environ.get('PORT', 'unknown')
|
||||
'port': os.environ.get('PORT', 'unknown'),
|
||||
'pricing_tier_name': os.environ.get('PRICING_TIER_NAME', 'unknown')
|
||||
}
|
||||
|
||||
return jsonify(version_info)
|
||||
@@ -591,6 +592,7 @@ def get_version_info(current_user):
|
||||
'app_version': 'unknown',
|
||||
'git_commit': 'unknown',
|
||||
'git_branch': 'unknown',
|
||||
'deployed_at': 'unknown'
|
||||
'deployed_at': 'unknown',
|
||||
'pricing_tier_name': 'unknown'
|
||||
}), 500
|
||||
|
||||
|
||||
@@ -2599,6 +2599,32 @@ async function checkStackExists(stackName) {
|
||||
// Add new function to deploy stack
|
||||
async function deployStack(dockerComposeContent, stackName, port) {
|
||||
try {
|
||||
// Get the launch data from sessionStorage to access pricing tier info
|
||||
const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData'));
|
||||
|
||||
// Fetch the pricing tier details to get the actual quota values
|
||||
let pricingTierDetails = null;
|
||||
if (launchData?.pricingTier?.id) {
|
||||
try {
|
||||
const pricingResponse = await fetch(`/api/admin/pricing-plans/${launchData.pricingTier.id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
});
|
||||
|
||||
if (pricingResponse.ok) {
|
||||
const pricingData = await pricingResponse.json();
|
||||
if (pricingData.success) {
|
||||
pricingTierDetails = pricingData.plan;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch pricing tier details:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// First, attempt to deploy the stack
|
||||
const response = await fetch('/api/admin/deploy-stack', {
|
||||
method: 'POST',
|
||||
@@ -2633,6 +2659,35 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
||||
{
|
||||
name: 'DEPLOYED_AT',
|
||||
value: new Date().toISOString()
|
||||
},
|
||||
// Pricing tier environment variables with actual quota values
|
||||
{
|
||||
name: 'PRICING_TIER_ID',
|
||||
value: launchData?.pricingTier?.id?.toString() || '0'
|
||||
},
|
||||
{
|
||||
name: 'PRICING_TIER_NAME',
|
||||
value: launchData?.pricingTier?.name || 'Unknown'
|
||||
},
|
||||
{
|
||||
name: 'ROOM_QUOTA',
|
||||
value: pricingTierDetails?.room_quota?.toString() || '0'
|
||||
},
|
||||
{
|
||||
name: 'CONVERSATION_QUOTA',
|
||||
value: pricingTierDetails?.conversation_quota?.toString() || '0'
|
||||
},
|
||||
{
|
||||
name: 'STORAGE_QUOTA_GB',
|
||||
value: pricingTierDetails?.storage_quota_gb?.toString() || '0'
|
||||
},
|
||||
{
|
||||
name: 'MANAGER_QUOTA',
|
||||
value: pricingTierDetails?.manager_quota?.toString() || '0'
|
||||
},
|
||||
{
|
||||
name: 'ADMIN_QUOTA',
|
||||
value: pricingTierDetails?.admin_quota?.toString() || '0'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -130,6 +130,18 @@
|
||||
<div class="company-value" id="company-description">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex">
|
||||
<div class="text-muted me-2" style="min-width: 120px;">Version:</div>
|
||||
<div class="company-value" id="instance-version-value">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex">
|
||||
<div class="text-muted me-2" style="min-width: 120px;">Payment Plan:</div>
|
||||
<div class="company-value" id="instance-payment-plan-value">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">Contact Information</h5>
|
||||
@@ -1569,5 +1581,46 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to fetch version and payment plan info
|
||||
async function fetchInstanceVersionAndPlan() {
|
||||
const versionEl = document.getElementById('instance-version-value');
|
||||
const planEl = document.getElementById('instance-payment-plan-value');
|
||||
versionEl.textContent = 'Loading...';
|
||||
planEl.textContent = 'Loading...';
|
||||
try {
|
||||
// Get JWT token
|
||||
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-API-Key': '{{ instance.connection_token }}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (!tokenResponse.ok) throw new Error('Failed to get management token');
|
||||
const tokenData = await tokenResponse.json();
|
||||
if (!tokenData.token) throw new Error('No token received');
|
||||
// Fetch version info
|
||||
const response = await fetch(`{{ instance.main_url }}/api/admin/version-info`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${tokenData.token}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to fetch version info');
|
||||
const data = await response.json();
|
||||
versionEl.textContent = data.app_version || 'Unknown';
|
||||
planEl.textContent = data.pricing_tier_name || 'Unknown';
|
||||
} catch (error) {
|
||||
versionEl.textContent = 'Error';
|
||||
planEl.textContent = 'Error';
|
||||
console.error('Error fetching version/plan info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// ... existing code ...
|
||||
fetchInstanceVersionAndPlan();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -49,6 +49,54 @@
|
||||
.badge.bg-orange:hover {
|
||||
background-color: #e55a00 !important;
|
||||
}
|
||||
|
||||
/* Pricing tier selection styles */
|
||||
.pricing-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.pricing-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.pricing-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background-color: rgba(22, 118, 123, 0.05);
|
||||
box-shadow: 0 4px 12px rgba(22, 118, 123, 0.2);
|
||||
}
|
||||
|
||||
.pricing-card.selected::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pricing-card.border-primary {
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.quota-info {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.features {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -246,6 +294,10 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Hidden fields for pricing tier selection -->
|
||||
<input type="hidden" id="selectedPricingTierId" value="">
|
||||
<input type="hidden" id="selectedPricingTierName" value="">
|
||||
|
||||
<!-- Steps Navigation -->
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<div class="step-item active" data-step="1">
|
||||
@@ -270,6 +322,10 @@
|
||||
</div>
|
||||
<div class="step-item" data-step="6">
|
||||
<div class="step-circle">6</div>
|
||||
<div class="step-label">Pricing Tier</div>
|
||||
</div>
|
||||
<div class="step-item" data-step="7">
|
||||
<div class="step-circle">7</div>
|
||||
<div class="step-label">Launch</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -479,8 +535,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 6 -->
|
||||
<!-- Step 6: Pricing Tier Selection -->
|
||||
<div class="step-pane" id="step6">
|
||||
<div class="step-content">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-4">Select Pricing Tier</h5>
|
||||
<p class="text-muted mb-4">Choose the pricing tier that best fits your needs. This will determine the resource limits for your instance.</p>
|
||||
|
||||
<div id="pricingTiersContainer" class="row">
|
||||
<!-- Pricing tiers will be loaded here -->
|
||||
<div class="col-12 text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading pricing tiers...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading available pricing tiers...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 7 -->
|
||||
<div class="step-pane" id="step7">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-rocket fa-4x mb-4" style="color: var(--primary-color);"></i>
|
||||
<h4>Ready to Launch!</h4>
|
||||
@@ -494,6 +572,7 @@
|
||||
<p><strong>Repository:</strong> <span id="reviewRepo"></span></p>
|
||||
<p><strong>Branch:</strong> <span id="reviewBranch"></span></p>
|
||||
<p><strong>Company:</strong> <span id="reviewCompany"></span></p>
|
||||
<p><strong>Pricing Tier:</strong> <span id="reviewPricingTier"></span></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Port:</strong> <span id="reviewPort"></span></p>
|
||||
@@ -723,7 +802,7 @@ let launchStepsModal;
|
||||
let currentStep = 1;
|
||||
|
||||
// Update the total number of steps
|
||||
const totalSteps = 6;
|
||||
const totalSteps = 7;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal'));
|
||||
@@ -998,6 +1077,7 @@ function compareSemanticVersions(currentVersion, latestVersion) {
|
||||
async function fetchVersionInfo(instanceUrl, instanceId) {
|
||||
const row = document.querySelector(`[data-instance-id="${instanceId}"]`).closest('tr');
|
||||
const versionCell = row.querySelector('td:nth-child(9)'); // Version column (adjusted after removing branch)
|
||||
const paymentPlanCell = row.querySelector('td:nth-child(6)'); // Payment Plan column
|
||||
|
||||
// Show loading state
|
||||
if (versionCell) {
|
||||
@@ -1033,6 +1113,25 @@ async function fetchVersionInfo(instanceUrl, instanceId) {
|
||||
const data = await response.json();
|
||||
console.log('Received version data:', data);
|
||||
|
||||
// Update payment plan cell with pricing tier name
|
||||
if (paymentPlanCell) {
|
||||
const pricingTierName = data.pricing_tier_name || 'unknown';
|
||||
if (pricingTierName !== 'unknown') {
|
||||
paymentPlanCell.innerHTML = `
|
||||
<span class="badge bg-info" data-bs-toggle="tooltip" title="Pricing Tier: ${pricingTierName}">
|
||||
<i class="fas fa-tag me-1"></i>${pricingTierName}
|
||||
</span>`;
|
||||
|
||||
// Add tooltip for payment plan
|
||||
const paymentPlanBadge = paymentPlanCell.querySelector('[data-bs-toggle="tooltip"]');
|
||||
if (paymentPlanBadge) {
|
||||
new bootstrap.Tooltip(paymentPlanBadge);
|
||||
}
|
||||
} else {
|
||||
paymentPlanCell.innerHTML = '<span class="badge bg-secondary">unknown</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Update version cell
|
||||
if (versionCell) {
|
||||
const appVersion = data.app_version || 'unknown';
|
||||
@@ -1130,11 +1229,23 @@ async function fetchVersionInfo(instanceUrl, instanceId) {
|
||||
</span>`;
|
||||
}
|
||||
|
||||
if (paymentPlanCell) {
|
||||
paymentPlanCell.innerHTML = `
|
||||
<span class="text-warning" data-bs-toggle="tooltip" title="Error: ${error.message}">
|
||||
<i class="fas fa-exclamation-triangle"></i> Error
|
||||
</span>`;
|
||||
}
|
||||
|
||||
// Add tooltips for error states
|
||||
const errorBadge = versionCell?.querySelector('[data-bs-toggle="tooltip"]');
|
||||
if (errorBadge) {
|
||||
new bootstrap.Tooltip(errorBadge);
|
||||
}
|
||||
|
||||
const paymentPlanErrorBadge = paymentPlanCell?.querySelector('[data-bs-toggle="tooltip"]');
|
||||
if (paymentPlanErrorBadge) {
|
||||
new bootstrap.Tooltip(paymentPlanErrorBadge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1650,6 +1761,11 @@ function updateStepDisplay() {
|
||||
if (currentStep === 4) {
|
||||
getNextAvailablePort();
|
||||
}
|
||||
|
||||
// If we're on step 6, load pricing tiers
|
||||
if (currentStep === 6) {
|
||||
loadPricingTiers();
|
||||
}
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
@@ -1662,6 +1778,9 @@ function nextStep() {
|
||||
if (currentStep === 4 && !validateStep4()) {
|
||||
return;
|
||||
}
|
||||
if (currentStep === 6 && !validateStep6()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep < totalSteps) {
|
||||
currentStep++;
|
||||
@@ -2066,6 +2185,7 @@ function updateReviewSection() {
|
||||
const webAddresses = Array.from(document.querySelectorAll('.web-address'))
|
||||
.map(input => input.value)
|
||||
.join(', ');
|
||||
const pricingTier = document.getElementById('selectedPricingTierName').value;
|
||||
|
||||
// Update the review section
|
||||
document.getElementById('reviewRepo').textContent = repo;
|
||||
@@ -2073,6 +2193,7 @@ function updateReviewSection() {
|
||||
document.getElementById('reviewCompany').textContent = company;
|
||||
document.getElementById('reviewPort').textContent = port;
|
||||
document.getElementById('reviewWebAddresses').textContent = webAddresses;
|
||||
document.getElementById('reviewPricingTier').textContent = pricingTier || 'Not selected';
|
||||
}
|
||||
|
||||
// Function to launch the instance
|
||||
@@ -2099,6 +2220,10 @@ function launchInstance() {
|
||||
colors: {
|
||||
primary: document.getElementById('primaryColor').value,
|
||||
secondary: document.getElementById('secondaryColor').value
|
||||
},
|
||||
pricingTier: {
|
||||
id: document.getElementById('selectedPricingTierId').value,
|
||||
name: document.getElementById('selectedPricingTierName').value
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2240,5 +2365,119 @@ async function refreshLatestVersion() {
|
||||
console.log('Manual refresh of latest version requested');
|
||||
await fetchLatestVersion();
|
||||
}
|
||||
|
||||
// Function to load pricing tiers
|
||||
async function loadPricingTiers() {
|
||||
const container = document.getElementById('pricingTiersContainer');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/pricing-plans', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load pricing tiers');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.plans && data.plans.length > 0) {
|
||||
container.innerHTML = data.plans.map(plan => `
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="card pricing-card h-100 ${plan.is_popular ? 'border-primary' : ''}"
|
||||
onclick="selectPricingTier(${plan.id}, '${plan.name}')">
|
||||
<div class="card-body text-center">
|
||||
${plan.is_popular ? '<div class="badge bg-primary mb-2">Most Popular</div>' : ''}
|
||||
<h5 class="card-title">${plan.name}</h5>
|
||||
${plan.description ? `<p class="text-muted small mb-3">${plan.description}</p>` : ''}
|
||||
|
||||
<div class="pricing mb-3">
|
||||
${plan.is_custom ?
|
||||
'<span class="h4 text-primary">Custom Pricing</span>' :
|
||||
`<span class="h4 text-primary">€${plan.monthly_price}</span><span class="text-muted">/month</span>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="quota-info small text-muted mb-3">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<i class="fas fa-door-open me-1"></i>${plan.format_quota_display.room_quota}<br>
|
||||
<i class="fas fa-comments me-1"></i>${plan.format_quota_display.conversation_quota}<br>
|
||||
<i class="fas fa-hdd me-1"></i>${plan.format_quota_display.storage_quota_gb}
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<i class="fas fa-user-tie me-1"></i>${plan.format_quota_display.manager_quota}<br>
|
||||
<i class="fas fa-user-shield me-1"></i>${plan.format_quota_display.admin_quota}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="features small">
|
||||
${plan.features.slice(0, 3).map(feature => `<div>✓ ${feature}</div>`).join('')}
|
||||
${plan.features.length > 3 ? `<div class="text-muted">+${plan.features.length - 3} more features</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="col-12 text-center">
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
No pricing tiers available. Please contact your administrator.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading pricing tiers:', error);
|
||||
container.innerHTML = `
|
||||
<div class="col-12 text-center">
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Error loading pricing tiers: ${error.message}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to select a pricing tier
|
||||
function selectPricingTier(planId, planName) {
|
||||
// Remove selection from all cards
|
||||
document.querySelectorAll('.pricing-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Add selection to clicked card
|
||||
event.currentTarget.classList.add('selected');
|
||||
|
||||
// Store the selection
|
||||
document.getElementById('selectedPricingTierId').value = planId;
|
||||
document.getElementById('selectedPricingTierName').value = planName;
|
||||
|
||||
// Enable next button if not already enabled
|
||||
const nextButton = document.querySelector('#launchStepsFooter .btn-primary');
|
||||
if (nextButton.disabled) {
|
||||
nextButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to validate step 6 (pricing tier selection)
|
||||
function validateStep6() {
|
||||
const selectedTierId = document.getElementById('selectedPricingTierId').value;
|
||||
|
||||
if (!selectedTierId) {
|
||||
alert('Please select a pricing tier before proceeding.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user