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
import sqlalchemy as sa
from sqlalchemy import text
# revision identifiers, used by Alembic.
@@ -17,23 +18,52 @@ 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')
)
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',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('company', sa.String(length=255), 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.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():
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')

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'
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)
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)
rooms_count = db.Column(db.Integer, nullable=False, default=0)
conversations_count = db.Column(db.Integer, nullable=False, default=0)
data_size = db.Column(db.Float, nullable=False, default=0.0)
payment_plan = db.Column(db.String(20), nullable=False, default='Basic')
main_url = db.Column(db.String(255), unique=True, nullable=False)
status = db.Column(db.String(20), nullable=False, default='inactive')
status_details = db.Column(db.Text, nullable=True)
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):
return f'<Instance {self.name}>'

View File

@@ -1,15 +1,16 @@
Flask>=2.0.0
Flask-SQLAlchemy>=3.0.0
Flask-Login>=0.6.0
Flask-WTF>=1.0.0
Flask-Mail==0.9.1
Flask-Migrate>=4.0.0
SQLAlchemy>=1.4.0
Werkzeug>=2.0.0
WTForms==3.1.1
Flask-WTF>=1.0.0
email-validator==2.1.0.post1
python-dotenv>=0.19.0
psycopg2-binary==2.9.9
gunicorn==21.2.0
email_validator==2.1.0.post1
Werkzeug>=2.0.0
SQLAlchemy>=1.4.0
alembic>=1.7.0
psycopg2-binary==2.9.9
requests>=2.31.0
gunicorn==21.2.0
prometheus-client>=0.16.0
PyJWT>=2.8.0

View File

@@ -16,6 +16,8 @@ import csv
from flask_wtf.csrf import generate_csrf
import json
import smtplib
import requests
from functools import wraps
# Set up logging to show in console
logging.basicConfig(
@@ -332,6 +334,30 @@ def init_routes(main_bp):
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')
@login_required
@require_password_change
@@ -341,6 +367,15 @@ def init_routes(main_bp):
return redirect(url_for('main.dashboard'))
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)
@main_bp.route('/instances/add', methods=['POST'])
@@ -431,6 +466,22 @@ def init_routes(main_bp):
db.session.rollback()
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'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)

View File

@@ -57,7 +57,10 @@
<td>{{ instance.payment_plan }}</td>
<td>{{ instance.main_url }}</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 }}
</span>
</td>
@@ -183,8 +186,68 @@ 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'));
// 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
function showAddInstanceModal() {
document.getElementById('addInstanceForm').reset();