Files
2026-06-28 19:00:34 -04:00

110 lines
3.6 KiB
Python

"""
control/panel/health.py
-----------------------
Superadmin health / monitoring dashboard (MT-8+).
GET /health — reads control plane data only (no per-tenant DB queries)
so it stays fast regardless of tenant count.
"""
import logging
import os
from flask import Blueprint, render_template, session
from control.base import control_session
from control.models import Plan, Tenant
from control.tenant_migrate import chain_head
from control.time_utils import now_eastern
from .decorators import superadmin_required
logger = logging.getLogger(__name__)
bp = Blueprint('health', __name__, url_prefix='/health')
_BASE_DOMAIN = lambda: os.environ.get('TENANT_BASE_DOMAIN', 'jqc.app')
@bp.route('/')
@superadmin_required
def dashboard():
try:
head = chain_head()
except Exception:
head = None
with control_session() as s:
plans = {p.id: p.name for p in s.query(Plan).all()}
tenants = s.query(Tenant).order_by(Tenant.id).all()
now = now_eastern()
rows = []
for t in tenants:
trial_days_left = None
trial_expired = False
if t.trial_ends_at:
delta = (t.trial_ends_at - now).days
trial_days_left = max(0, delta)
trial_expired = now >= t.trial_ends_at
schema_ok = (t.alembic_head == head) if head else None
rows.append({
'id': t.id,
'slug': t.slug,
'name': t.name,
'status': t.status,
'plan_name': plans.get(t.plan_id, '?'),
'subscription_status': t.subscription_status,
'trial_ends_at': t.trial_ends_at,
'trial_days_left': trial_days_left,
'trial_expired': trial_expired,
'current_period_end': t.current_period_end,
'has_stripe': bool(t.stripe_customer_id),
'alembic_head': t.alembic_head,
'schema_ok': schema_ok,
'created_at': t.created_at,
'suspended_at': t.suspended_at,
})
# ── Aggregate stats ──────────────────────────────────────────────────────
total = len(rows)
active = sum(1 for r in rows if r['status'] == 'active')
suspended = sum(1 for r in rows if r['status'] == 'suspended')
schema_behind = sum(1 for r in rows if r['schema_ok'] is False)
sub_counts = {}
for r in rows:
key = r['subscription_status'] or 'none'
sub_counts[key] = sub_counts.get(key, 0) + 1
trial_expiring_soon = sum(
1 for r in rows
if r['subscription_status'] == 'trial'
and r['trial_days_left'] is not None
and r['trial_days_left'] <= 3
and not r['trial_expired']
)
trial_expired_count = sum(
1 for r in rows
if r['subscription_status'] == 'trial' and r['trial_expired']
)
stats = {
'total': total,
'active': active,
'suspended': suspended,
'schema_behind': schema_behind,
'sub_counts': sub_counts,
'trial_expiring_soon': trial_expiring_soon,
'trial_expired': trial_expired_count,
}
return render_template(
'panel/health.html',
rows=rows,
stats=stats,
chain_head=head,
sa_username=session.get('sa_username'),
base_domain=_BASE_DOMAIN(),
)