Active status check

This commit is contained in:
2025-06-09 10:24:42 +02:00
parent 112a99ffcb
commit 2014c326b1
9 changed files with 227 additions and 33 deletions

Binary file not shown.

View 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

View File

@@ -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():
op.create_table('instances', conn = op.get_bind()
sa.Column('id', sa.Integer(), nullable=False), result = conn.execute(text("""
sa.Column('name', sa.String(length=100), nullable=False), SELECT EXISTS (
sa.Column('company', sa.String(length=100), nullable=False), SELECT FROM information_schema.tables
sa.Column('rooms_count', sa.Integer(), nullable=False, server_default='0'), WHERE table_name = 'instances'
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'), exists = result.scalar()
sa.Column('main_url', sa.String(length=255), nullable=False), if not exists:
sa.Column('status', sa.String(length=20), nullable=False, server_default='inactive'), op.create_table('instances',
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')), sa.Column('name', sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint('id'), sa.Column('company', sa.String(length=255), nullable=False),
sa.UniqueConstraint('name'), sa.Column('rooms_count', sa.Integer(), nullable=False, server_default='0'),
sa.UniqueConstraint('main_url') sa.Column('conversations_count', sa.Integer(), nullable=False, server_default='0'),
) sa.Column('data_size', sa.String(length=50), nullable=False, server_default='0 MB'),
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('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')),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name'),
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')

View 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')

View File

@@ -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}>'

View File

@@ -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

View File

@@ -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)

View File

@@ -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();