adding instances

This commit is contained in:
2025-06-09 09:54:37 +02:00
parent 7aa96119a9
commit 112a99ffcb
9 changed files with 523 additions and 14 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,39 @@
"""add instances table
Revision ID: add_instances_table
Revises:
Create Date: 2024-03-19 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'add_instances_table'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.create_table('instances',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('company', sa.String(length=100), nullable=False),
sa.Column('rooms_count', sa.Integer(), nullable=False, server_default='0'),
sa.Column('conversations_count', sa.Integer(), nullable=False, server_default='0'),
sa.Column('data_size', sa.Float(), nullable=False, server_default='0.0'),
sa.Column('payment_plan', sa.String(length=20), nullable=False, server_default='Basic'),
sa.Column('main_url', sa.String(length=255), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False, server_default='inactive'),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name'),
sa.UniqueConstraint('main_url')
)
def downgrade():
op.drop_table('instances')

View File

@@ -493,4 +493,22 @@ class ManagementAPIKey(db.Model):
creator = db.relationship('User', backref=db.backref('created_api_keys', cascade='all, delete-orphan'))
def __repr__(self):
return f'<ManagementAPIKey {self.name}>'
return f'<ManagementAPIKey {self.name}>'
class Instance(db.Model):
__tablename__ = 'instances'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
company = db.Column(db.String(100), nullable=False)
rooms_count = db.Column(db.Integer, default=0)
conversations_count = db.Column(db.Integer, default=0)
data_size = db.Column(db.Float, default=0) # in GB
payment_plan = db.Column(db.String(50), nullable=False)
main_url = db.Column(db.String(255), nullable=False)
status = db.Column(db.String(20), default='inactive') # active or inactive
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def __repr__(self):
return f'<Instance {self.name}>'

View File

@@ -1,6 +1,6 @@
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session, current_app
from flask_login import current_user, login_required
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings, DocuPulseSettings, PasswordSetupToken
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings, DocuPulseSettings, PasswordSetupToken, Instance
from routes.auth import require_password_change
import os
from werkzeug.utils import secure_filename
@@ -339,7 +339,97 @@ def init_routes(main_bp):
if not os.environ.get('MASTER', 'false').lower() == 'true':
flash('This page is only available in master instances.', 'error')
return redirect(url_for('main.dashboard'))
return render_template('main/instances.html')
instances = Instance.query.all()
return render_template('main/instances.html', instances=instances)
@main_bp.route('/instances/add', methods=['POST'])
@login_required
@require_password_change
def add_instance():
if not os.environ.get('MASTER', 'false').lower() == 'true':
return jsonify({'error': 'Unauthorized'}), 403
data = request.get_json()
try:
instance = Instance(
name=data['name'],
company=data['company'],
payment_plan=data['payment_plan'],
main_url=data['main_url'],
status='inactive' # New instances start as inactive
)
db.session.add(instance)
db.session.commit()
return jsonify({
'message': 'Instance added successfully',
'instance': {
'id': instance.id,
'name': instance.name,
'company': instance.company,
'rooms_count': instance.rooms_count,
'conversations_count': instance.conversations_count,
'data_size': instance.data_size,
'payment_plan': instance.payment_plan,
'main_url': instance.main_url,
'status': instance.status
}
})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 400
@main_bp.route('/instances/<int:instance_id>', methods=['PUT'])
@login_required
@require_password_change
def update_instance(instance_id):
if not os.environ.get('MASTER', 'false').lower() == 'true':
return jsonify({'error': 'Unauthorized'}), 403
instance = Instance.query.get_or_404(instance_id)
data = request.get_json()
try:
instance.name = data.get('name', instance.name)
instance.company = data.get('company', instance.company)
instance.payment_plan = data.get('payment_plan', instance.payment_plan)
instance.main_url = data.get('main_url', instance.main_url)
instance.status = data.get('status', instance.status)
db.session.commit()
return jsonify({
'message': 'Instance updated successfully',
'instance': {
'id': instance.id,
'name': instance.name,
'company': instance.company,
'rooms_count': instance.rooms_count,
'conversations_count': instance.conversations_count,
'data_size': instance.data_size,
'payment_plan': instance.payment_plan,
'main_url': instance.main_url,
'status': instance.status
}
})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 400
@main_bp.route('/instances/<int:instance_id>', methods=['DELETE'])
@login_required
@require_password_change
def delete_instance(instance_id):
if not os.environ.get('MASTER', 'false').lower() == 'true':
return jsonify({'error': 'Unauthorized'}), 403
instance = Instance.query.get_or_404(instance_id)
try:
db.session.delete(instance)
db.session.commit()
return jsonify({'message': 'Instance deleted successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 400
UPLOAD_FOLDER = '/app/uploads/profile_pics'
if not os.path.exists(UPLOAD_FOLDER):

View File

@@ -1,4 +1,4 @@
{% macro header(title, description="", button_text="", button_url="", icon="fa-folder", button_class="", button_icon="fa-plus", button_style="") %}
{% macro header(title, description="", button_text="", button_url="", icon="fa-folder", button_class="", button_icon="fa-plus", button_style="", buttons=None) %}
<header class="py-4">
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center">
@@ -11,8 +11,31 @@
<p class="text-muted mb-0 mt-2 small">{{ description }}</p>
{% endif %}
</div>
{% if button_text and button_url %}
<div>
<div class="d-flex gap-2">
{% if buttons %}
{% for button in buttons %}
{% if button.url == "#" %}
<button id="{{ button.id if button.id else '' }}"
class="btn {{ button.class if button.class else '' }}"
style="{{ 'background-color: var(--primary-color); color: white;' if not button.class else '' }}{{ '; ' + button.style if button.style else '' }}"
onclick="{{ button.onclick if button.onclick else '' }}">
{% if button.icon %}
<i class="fas {{ button.icon }} me-1"></i>
{% endif %}
{{ button.text }}
</button>
{% else %}
<button onclick="window.location.href='{{ button.url }}'"
class="btn {{ button.class if button.class else '' }}"
style="{{ 'background-color: var(--primary-color); color: white;' if not button.class else '' }}{{ '; ' + button.style if button.style else '' }}">
{% if button.icon %}
<i class="fas {{ button.icon }} me-1"></i>
{% endif %}
{{ button.text }}
</button>
{% endif %}
{% endfor %}
{% elif button_text and button_url %}
{% if button_url == "#" %}
<button id="emptyTrashBtn"
class="btn {{ button_class if button_class else '' }}"
@@ -33,8 +56,8 @@
{{ button_text }}
</button>
{% endif %}
</div>
{% endif %}
{% endif %}
</div>
</div>
</div>
</header>

View File

@@ -7,14 +7,344 @@
{{ header(
title="Instances",
description="Manage your DocuPulse instances",
button_text="",
button_url="",
icon="fa-server"
icon="fa-server",
buttons=[
{
'text': 'Launch New Instance',
'url': '#',
'icon': 'fa-rocket',
'class': 'btn-primary',
'onclick': 'showAddInstanceModal()'
},
{
'text': 'Add Existing Instance',
'url': '#',
'icon': 'fa-link',
'class': 'btn-primary',
'onclick': 'showAddExistingInstanceModal()'
}
]
) }}
<div class="container mx-auto px-4 py-8">
<div class="text-center">
<p class="text-muted">Instance management will be available soon.</p>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Company</th>
<th>Rooms</th>
<th>Conversations</th>
<th>Data</th>
<th>Payment Plan</th>
<th>Main URL</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for instance in instances %}
<tr>
<td>{{ instance.name }}</td>
<td>{{ instance.company }}</td>
<td>{{ instance.rooms_count }}</td>
<td>{{ instance.conversations_count }}</td>
<td>{{ "%.1f"|format(instance.data_size) }} GB</td>
<td>{{ instance.payment_plan }}</td>
<td>{{ instance.main_url }}</td>
<td>
<span class="badge bg-{{ 'success' if instance.status == 'active' else 'danger' }}">
{{ instance.status|title }}
</span>
</td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editInstance({{ instance.id }})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteInstance({{ instance.id }})">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Instance Modal -->
<div class="modal fade" id="addInstanceModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Launch New Instance</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addInstanceForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="name" class="form-label">Instance Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="main_url" class="form-label">Main URL</label>
<input type="url" class="form-control" id="main_url" name="main_url" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitAddInstance()">Launch Instance</button>
</div>
</div>
</div>
</div>
<!-- Edit Instance Modal -->
<div class="modal fade" id="editInstanceModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Instance</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editInstanceForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" id="edit_instance_id">
<div class="mb-3">
<label for="edit_name" class="form-label">Instance Name</label>
<input type="text" class="form-control" id="edit_name" name="name" required>
</div>
<div class="mb-3">
<label for="edit_main_url" class="form-label">Main URL</label>
<input type="url" class="form-control" id="edit_main_url" name="main_url" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitEditInstance()">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- Add Existing Instance Modal -->
<div class="modal fade" id="addExistingInstanceModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Existing Instance</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addExistingInstanceForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="mb-3">
<label for="existing_url" class="form-label">Instance URL</label>
<input type="url" class="form-control" id="existing_url" name="url" required onchange="updateInstanceFields()">
</div>
<div class="mb-3">
<label for="existing_name" class="form-label">Instance Name</label>
<input type="text" class="form-control" id="existing_name" name="name" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitAddExistingInstance()">Add Instance</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Modal instances
let addInstanceModal;
let editInstanceModal;
let addExistingInstanceModal;
document.addEventListener('DOMContentLoaded', function() {
addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal'));
editInstanceModal = new bootstrap.Modal(document.getElementById('editInstanceModal'));
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
});
// Show modals
function showAddInstanceModal() {
document.getElementById('addInstanceForm').reset();
addInstanceModal.show();
}
function showAddExistingInstanceModal() {
document.getElementById('addExistingInstanceForm').reset();
addExistingInstanceModal.show();
}
// CRUD operations
async function submitAddInstance() {
const form = document.getElementById('addInstanceForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
try {
const response = await fetch('/instances/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': formData.get('csrf_token')
},
body: JSON.stringify({
name: data.name,
company: data.name.charAt(0).toUpperCase() + data.name.slice(1),
rooms_count: 0,
conversations_count: 0,
data_size: 0.0,
payment_plan: 'Basic',
main_url: data.main_url,
status: 'inactive'
})
});
if (!response.ok) throw new Error('Failed to add instance');
const result = await response.json();
addInstanceModal.hide();
location.reload(); // Refresh to show new instance
} catch (error) {
alert('Error adding instance: ' + error.message);
}
}
async function editInstance(id) {
try {
const response = await fetch(`/instances/${id}`);
if (!response.ok) throw new Error('Failed to fetch instance');
const instance = await response.json();
// Populate form
document.getElementById('edit_instance_id').value = instance.id;
document.getElementById('edit_name').value = instance.name;
document.getElementById('edit_main_url').value = instance.main_url;
editInstanceModal.show();
} catch (error) {
alert('Error fetching instance: ' + error.message);
}
}
async function submitEditInstance() {
const form = document.getElementById('editInstanceForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const id = document.getElementById('edit_instance_id').value;
try {
const response = await fetch(`/instances/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': formData.get('csrf_token')
},
body: JSON.stringify({
name: data.name,
company: data.name.charAt(0).toUpperCase() + data.name.slice(1),
main_url: data.main_url
})
});
if (!response.ok) throw new Error('Failed to update instance');
editInstanceModal.hide();
location.reload(); // Refresh to show updated instance
} catch (error) {
alert('Error updating instance: ' + error.message);
}
}
async function deleteInstance(id) {
if (!confirm('Are you sure you want to delete this instance?')) return;
const form = document.getElementById('editInstanceForm');
const formData = new FormData(form);
try {
const response = await fetch(`/instances/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-Token': formData.get('csrf_token')
}
});
if (!response.ok) throw new Error('Failed to delete instance');
location.reload(); // Refresh to show updated list
} catch (error) {
alert('Error deleting instance: ' + error.message);
}
}
function updateInstanceFields() {
const url = document.getElementById('existing_url').value;
if (url) {
try {
const urlObj = new URL(url);
const hostname = urlObj.hostname;
const name = hostname.split('.')[0];
document.getElementById('existing_name').value = name;
} catch (e) {
console.error('Invalid URL:', e);
}
}
}
async function submitAddExistingInstance() {
const form = document.getElementById('addExistingInstanceForm');
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
try {
const response = await fetch('/instances/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': formData.get('csrf_token')
},
body: JSON.stringify({
name: data.name,
company: data.name.charAt(0).toUpperCase() + data.name.slice(1),
rooms_count: 0,
conversations_count: 0,
data_size: 0.0,
payment_plan: 'Basic',
main_url: data.url,
status: 'inactive'
})
});
if (!response.ok) throw new Error('Failed to add instance');
addExistingInstanceModal.hide();
location.reload(); // Refresh to show new instance
} catch (error) {
alert('Error adding instance: ' + error.message);
}
}
</script>
{% endblock %}

View File

@@ -35,6 +35,7 @@
<i class="fas fa-palette me-2"></i>Theme Colors
</button>
</li>
{% if not is_master %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'general' %}active{% endif %}" id="general-tab" data-bs-toggle="tab" data-bs-target="#general" type="button" role="tab" aria-controls="general" aria-selected="{{ 'true' if active_tab == 'general' else 'false' }}">
<i class="fas fa-building me-2"></i>Company Info
@@ -45,11 +46,13 @@
<i class="fas fa-envelope me-2"></i>Email Templates
</button>
</li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'mails' %}active{% endif %}" id="mails-tab" data-bs-toggle="tab" data-bs-target="#mails" type="button" role="tab" aria-controls="mails" aria-selected="{{ 'true' if active_tab == 'mails' else 'false' }}">
<i class="fas fa-paper-plane me-2"></i>Mail Log
</button>
</li>
{% if not is_master %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'security' %}active{% endif %}" id="security-tab" data-bs-toggle="tab" data-bs-target="#security" type="button" role="tab" aria-controls="security" aria-selected="{{ 'true' if active_tab == 'security' else 'false' }}">
<i class="fas fa-shield-alt me-2"></i>Security
@@ -60,11 +63,13 @@
<i class="fas fa-history me-2"></i>Event Log
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'debugging' %}active{% endif %}" id="debugging-tab" data-bs-toggle="tab" data-bs-target="#debugging" type="button" role="tab" aria-controls="debugging" aria-selected="{{ 'true' if active_tab == 'debugging' else 'false' }}">
<i class="fas fa-bug me-2"></i>Debugging
</button>
</li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'smtp' %}active{% endif %}" id="smtp-tab" data-bs-toggle="tab" data-bs-target="#smtp" type="button" role="tab" aria-controls="smtp" aria-selected="{{ 'true' if active_tab == 'smtp' else 'false' }}">
<i class="fas fa-server me-2"></i>SMTP
@@ -79,6 +84,7 @@
{{ colors_tab(primary_color, secondary_color, csrf_token) }}
</div>
{% if not is_master %}
<!-- Company Info Tab -->
<div class="tab-pane fade {% if active_tab == 'general' %}show active{% endif %}" id="general" role="tabpanel" aria-labelledby="general-tab">
{{ company_info_tab(site_settings, form, csrf_token) }}
@@ -88,12 +94,14 @@
<div class="tab-pane fade {% if active_tab == 'email_templates' %}show active{% endif %}" id="email-templates" role="tabpanel" aria-labelledby="email-templates-tab">
{{ email_templates_tab(email_templates, csrf_token) }}
</div>
{% endif %}
<!-- Mails Tab -->
<div class="tab-pane fade {% if active_tab == 'mails' %}show active{% endif %}" id="mails" role="tabpanel" aria-labelledby="mails-tab">
{{ mails_tab(mails, csrf_token, users, total_pages, current_page) }}
</div>
{% if not is_master %}
<!-- Security Tab -->
<div class="tab-pane fade {% if active_tab == 'security' %}show active{% endif %}" id="security" role="tabpanel" aria-labelledby="security-tab">
{{ security_tab() }}
@@ -103,6 +111,7 @@
<div class="tab-pane fade {% if active_tab == 'events' %}show active{% endif %}" id="events" role="tabpanel" aria-labelledby="events-tab">
{{ events_tab(events, csrf_token, users, total_pages, current_page) }}
</div>
{% endif %}
<!-- Debugging Tab -->
<div class="tab-pane fade {% if active_tab == 'debugging' %}show active{% endif %}" id="debugging" role="tabpanel" aria-labelledby="debugging-tab">