Active status check
This commit is contained in:
Binary file not shown.
24
migrations/versions/4ee23cb29001_merge_heads.py
Normal file
24
migrations/versions/4ee23cb29001_merge_heads.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""merge heads
|
||||||
|
|
||||||
|
Revision ID: 4ee23cb29001
|
||||||
|
Revises: 72ab6c4c6a5f, add_status_details
|
||||||
|
Create Date: 2025-06-09 10:04:48.708415
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4ee23cb29001'
|
||||||
|
down_revision = ('72ab6c4c6a5f', 'add_status_details')
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
||||||
@@ -7,6 +7,7 @@ Create Date: 2024-03-19 10:00:00.000000
|
|||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
@@ -17,23 +18,52 @@ depends_on = None
|
|||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
|
conn = op.get_bind()
|
||||||
|
result = conn.execute(text("""
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_name = 'instances'
|
||||||
|
);
|
||||||
|
"""))
|
||||||
|
exists = result.scalar()
|
||||||
|
if not exists:
|
||||||
op.create_table('instances',
|
op.create_table('instances',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('name', sa.String(length=100), nullable=False),
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
sa.Column('company', sa.String(length=100), nullable=False),
|
sa.Column('company', sa.String(length=255), nullable=False),
|
||||||
sa.Column('rooms_count', sa.Integer(), nullable=False, server_default='0'),
|
sa.Column('rooms_count', sa.Integer(), nullable=False, server_default='0'),
|
||||||
sa.Column('conversations_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('data_size', sa.String(length=50), nullable=False, server_default='0 MB'),
|
||||||
sa.Column('payment_plan', sa.String(length=20), nullable=False, server_default='Basic'),
|
sa.Column('payment_plan', sa.String(length=50), nullable=False, server_default='free'),
|
||||||
sa.Column('main_url', sa.String(length=255), nullable=False),
|
sa.Column('main_url', sa.String(length=255), nullable=False),
|
||||||
sa.Column('status', sa.String(length=20), nullable=False, server_default='inactive'),
|
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('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.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id'),
|
||||||
sa.UniqueConstraint('name'),
|
sa.UniqueConstraint('name'),
|
||||||
sa.UniqueConstraint('main_url')
|
sa.UniqueConstraint('main_url')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Create a trigger to automatically update the updated_at column
|
||||||
|
op.execute("""
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
""")
|
||||||
|
|
||||||
|
op.execute("""
|
||||||
|
CREATE TRIGGER update_instances_updated_at
|
||||||
|
BEFORE UPDATE ON instances
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
|
op.execute("DROP TRIGGER IF EXISTS update_instances_updated_at ON instances")
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS update_updated_at_column()")
|
||||||
op.drop_table('instances')
|
op.drop_table('instances')
|
||||||
24
migrations/versions/add_status_details.py
Normal file
24
migrations/versions/add_status_details.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""add status_details column
|
||||||
|
|
||||||
|
Revision ID: add_status_details
|
||||||
|
Revises: add_instances_table
|
||||||
|
Create Date: 2024-03-19 11:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_status_details'
|
||||||
|
down_revision = 'add_instances_table'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column('instances', sa.Column('status_details', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('instances', 'status_details')
|
||||||
19
models.py
19
models.py
@@ -499,16 +499,17 @@ class Instance(db.Model):
|
|||||||
__tablename__ = 'instances'
|
__tablename__ = 'instances'
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(100), nullable=False)
|
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
company = db.Column(db.String(100), nullable=False)
|
company = db.Column(db.String(100), nullable=False)
|
||||||
rooms_count = db.Column(db.Integer, default=0)
|
rooms_count = db.Column(db.Integer, nullable=False, default=0)
|
||||||
conversations_count = db.Column(db.Integer, default=0)
|
conversations_count = db.Column(db.Integer, nullable=False, default=0)
|
||||||
data_size = db.Column(db.Float, default=0) # in GB
|
data_size = db.Column(db.Float, nullable=False, default=0.0)
|
||||||
payment_plan = db.Column(db.String(50), nullable=False)
|
payment_plan = db.Column(db.String(20), nullable=False, default='Basic')
|
||||||
main_url = db.Column(db.String(255), nullable=False)
|
main_url = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
status = db.Column(db.String(20), default='inactive') # active or inactive
|
status = db.Column(db.String(20), nullable=False, default='inactive')
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
status_details = db.Column(db.Text, nullable=True)
|
||||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP'))
|
||||||
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Instance {self.name}>'
|
return f'<Instance {self.name}>'
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
Flask>=2.0.0
|
Flask>=2.0.0
|
||||||
Flask-SQLAlchemy>=3.0.0
|
Flask-SQLAlchemy>=3.0.0
|
||||||
Flask-Login>=0.6.0
|
Flask-Login>=0.6.0
|
||||||
Flask-WTF>=1.0.0
|
Flask-Mail==0.9.1
|
||||||
Flask-Migrate>=4.0.0
|
Flask-Migrate>=4.0.0
|
||||||
SQLAlchemy>=1.4.0
|
Flask-WTF>=1.0.0
|
||||||
Werkzeug>=2.0.0
|
email-validator==2.1.0.post1
|
||||||
WTForms==3.1.1
|
|
||||||
python-dotenv>=0.19.0
|
python-dotenv>=0.19.0
|
||||||
psycopg2-binary==2.9.9
|
Werkzeug>=2.0.0
|
||||||
gunicorn==21.2.0
|
SQLAlchemy>=1.4.0
|
||||||
email_validator==2.1.0.post1
|
|
||||||
alembic>=1.7.0
|
alembic>=1.7.0
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
|
requests>=2.31.0
|
||||||
|
gunicorn==21.2.0
|
||||||
prometheus-client>=0.16.0
|
prometheus-client>=0.16.0
|
||||||
PyJWT>=2.8.0
|
PyJWT>=2.8.0
|
||||||
Binary file not shown.
@@ -16,6 +16,8 @@ import csv
|
|||||||
from flask_wtf.csrf import generate_csrf
|
from flask_wtf.csrf import generate_csrf
|
||||||
import json
|
import json
|
||||||
import smtplib
|
import smtplib
|
||||||
|
import requests
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
# Set up logging to show in console
|
# Set up logging to show in console
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -332,6 +334,30 @@ def init_routes(main_bp):
|
|||||||
is_admin=current_user.is_admin
|
is_admin=current_user.is_admin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def check_instance_status(instance):
|
||||||
|
"""Check the status of an instance by contacting its health endpoint"""
|
||||||
|
try:
|
||||||
|
# Construct the health check URL
|
||||||
|
health_url = f"{instance.main_url.rstrip('/')}/health"
|
||||||
|
response = requests.get(health_url, timeout=5)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
return {
|
||||||
|
'status': 'active' if data.get('status') == 'healthy' else 'inactive',
|
||||||
|
'details': json.dumps(data) # Convert dictionary to JSON string
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'status': 'inactive',
|
||||||
|
'details': f"Health check returned status code {response.status_code}"
|
||||||
|
}
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return {
|
||||||
|
'status': 'inactive',
|
||||||
|
'details': str(e)
|
||||||
|
}
|
||||||
|
|
||||||
@main_bp.route('/instances')
|
@main_bp.route('/instances')
|
||||||
@login_required
|
@login_required
|
||||||
@require_password_change
|
@require_password_change
|
||||||
@@ -341,6 +367,15 @@ def init_routes(main_bp):
|
|||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
instances = Instance.query.all()
|
instances = Instance.query.all()
|
||||||
|
|
||||||
|
# Check status for each instance
|
||||||
|
for instance in instances:
|
||||||
|
status_info = check_instance_status(instance)
|
||||||
|
instance.status = status_info['status']
|
||||||
|
instance.status_details = status_info['details']
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
return render_template('main/instances.html', instances=instances)
|
return render_template('main/instances.html', instances=instances)
|
||||||
|
|
||||||
@main_bp.route('/instances/add', methods=['POST'])
|
@main_bp.route('/instances/add', methods=['POST'])
|
||||||
@@ -431,6 +466,22 @@ def init_routes(main_bp):
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({'error': str(e)}), 400
|
return jsonify({'error': str(e)}), 400
|
||||||
|
|
||||||
|
@main_bp.route('/instances/<int:instance_id>/status')
|
||||||
|
@login_required
|
||||||
|
def check_status(instance_id):
|
||||||
|
if not os.environ.get('MASTER', 'false').lower() == 'true':
|
||||||
|
return jsonify({'error': 'Access denied'}), 403
|
||||||
|
|
||||||
|
instance = Instance.query.get_or_404(instance_id)
|
||||||
|
status_info = check_instance_status(instance)
|
||||||
|
|
||||||
|
# Update instance status in database
|
||||||
|
instance.status = status_info['status']
|
||||||
|
instance.status_details = status_info['details']
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(status_info)
|
||||||
|
|
||||||
UPLOAD_FOLDER = '/app/uploads/profile_pics'
|
UPLOAD_FOLDER = '/app/uploads/profile_pics'
|
||||||
if not os.path.exists(UPLOAD_FOLDER):
|
if not os.path.exists(UPLOAD_FOLDER):
|
||||||
os.makedirs(UPLOAD_FOLDER)
|
os.makedirs(UPLOAD_FOLDER)
|
||||||
|
|||||||
@@ -57,7 +57,10 @@
|
|||||||
<td>{{ instance.payment_plan }}</td>
|
<td>{{ instance.payment_plan }}</td>
|
||||||
<td>{{ instance.main_url }}</td>
|
<td>{{ instance.main_url }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-{{ 'success' if instance.status == 'active' else 'danger' }}">
|
<span class="badge bg-{{ 'success' if instance.status == 'active' else 'danger' }}"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-instance-id="{{ instance.id }}"
|
||||||
|
title="{{ instance.status_details }}">
|
||||||
{{ instance.status|title }}
|
{{ instance.status|title }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -183,8 +186,68 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal'));
|
addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal'));
|
||||||
editInstanceModal = new bootstrap.Modal(document.getElementById('editInstanceModal'));
|
editInstanceModal = new bootstrap.Modal(document.getElementById('editInstanceModal'));
|
||||||
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
|
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
|
||||||
|
|
||||||
|
// Initialize tooltips
|
||||||
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check statuses on page load
|
||||||
|
checkAllInstanceStatuses();
|
||||||
|
|
||||||
|
// Set up periodic status checks (every 30 seconds)
|
||||||
|
setInterval(checkAllInstanceStatuses, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Function to check status of all instances
|
||||||
|
async function checkAllInstanceStatuses() {
|
||||||
|
const statusBadges = document.querySelectorAll('[data-instance-id]');
|
||||||
|
for (const badge of statusBadges) {
|
||||||
|
const instanceId = badge.dataset.instanceId;
|
||||||
|
await checkInstanceStatus(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check status of a single instance
|
||||||
|
async function checkInstanceStatus(instanceId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/instances/${instanceId}/status`);
|
||||||
|
if (!response.ok) throw new Error('Failed to check instance status');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const badge = document.querySelector(`[data-instance-id="${instanceId}"]`);
|
||||||
|
if (badge) {
|
||||||
|
badge.className = `badge bg-${data.status === 'active' ? 'success' : 'danger'}`;
|
||||||
|
badge.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1);
|
||||||
|
|
||||||
|
// Parse the JSON string in status_details
|
||||||
|
let tooltipContent = data.status;
|
||||||
|
if (data.status_details) {
|
||||||
|
try {
|
||||||
|
const details = JSON.parse(data.status_details);
|
||||||
|
tooltipContent = `Status: ${details.status}\nTimestamp: ${details.timestamp}`;
|
||||||
|
if (details.database) {
|
||||||
|
tooltipContent += `\nDatabase: ${details.database}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
tooltipContent = data.status_details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
badge.title = tooltipContent;
|
||||||
|
|
||||||
|
// Update tooltip
|
||||||
|
const tooltip = bootstrap.Tooltip.getInstance(badge);
|
||||||
|
if (tooltip) {
|
||||||
|
tooltip.dispose();
|
||||||
|
}
|
||||||
|
new bootstrap.Tooltip(badge);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking instance status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show modals
|
// Show modals
|
||||||
function showAddInstanceModal() {
|
function showAddInstanceModal() {
|
||||||
document.getElementById('addInstanceForm').reset();
|
document.getElementById('addInstanceForm').reset();
|
||||||
|
|||||||
Reference in New Issue
Block a user