""" Monitors API Uptime/synthetic checks against URLs, hosts and managed sites. Core, not part of the serverkit-status extension — watching a site must not require publishing a status page. See app/services/monitor_service.py. """ from flask import Blueprint, request, jsonify from flask_jwt_extended import jwt_required from app.exceptions import NotFoundError from app.middleware.rbac import permission_required from app.services.monitor_service import MonitorService from app.api._request import json_object monitors_bp = Blueprint('monitors', __name__) @monitors_bp.route('', methods=['GET']) @monitors_bp.route('/', methods=['GET']) @jwt_required() def list_monitors(): """List monitors. Query params: status, type, q, page_id. """ page_id = request.args.get('page_id', type=int) monitors = MonitorService.list_monitors( status=request.args.get('status') or None, check_type=request.args.get('type') or None, q=request.args.get('q') or None, page_id=page_id, ) # One extra query for every row's sparkline, not one per row. sparks = MonitorService.recent_response_times([m.id for m in monitors]) payload = [] for monitor in monitors: item = monitor.to_dict() item['spark'] = sparks.get(monitor.id, []) payload.append(item) return jsonify({'monitors': payload}) @monitors_bp.route('/stats', methods=['GET']) @jwt_required() def get_stats(): """KPI-band counts for the Monitors tab.""" return jsonify({'stats': MonitorService.stats()}) @monitors_bp.route('', methods=['POST']) @monitors_bp.route('/', methods=['POST']) @permission_required('monitoring', 'write') def create_monitor(): data = json_object() monitor = MonitorService.create(data) return jsonify(monitor.to_dict()), 201 @monitors_bp.route('/', methods=['GET']) @jwt_required() def get_monitor(monitor_id): monitor = MonitorService.get(monitor_id) if not monitor: raise NotFoundError('Monitor not found', code='monitor_not_found') return jsonify(monitor.to_dict()) @monitors_bp.route('/', methods=['PATCH', 'PUT']) @permission_required('monitoring', 'write') def update_monitor(monitor_id): data = json_object() monitor = MonitorService.update(monitor_id, data) if not monitor: raise NotFoundError('Monitor not found', code='monitor_not_found') return jsonify(monitor.to_dict()) @monitors_bp.route('/', methods=['DELETE']) @permission_required('monitoring', 'write') def delete_monitor(monitor_id): if not MonitorService.delete(monitor_id): return {'error': 'Monitor not found'}, 404 return jsonify({'deleted': True}) @monitors_bp.route('//check', methods=['POST']) @permission_required('monitoring', 'write') def run_check(monitor_id): """Probe now, out of band from the scheduler.""" hc = MonitorService.run_check(monitor_id) if not hc: return {'error': 'Monitor not found'}, 404 monitor = MonitorService.get(monitor_id) return jsonify({'check': hc.to_dict(), 'monitor': monitor.to_dict()}) @monitors_bp.route('//pause', methods=['POST']) @permission_required('monitoring', 'write') def set_paused(monitor_id): data = json_object() monitor = MonitorService.set_paused(monitor_id, data.get('paused', True)) if not monitor: return {'error': 'Monitor not found'}, 404 return jsonify(monitor.to_dict()) @monitors_bp.route('//history', methods=['GET']) @jwt_required() def get_history(monitor_id): """Recent check results. Query params: hours (default 24), limit.""" if not MonitorService.get(monitor_id): return {'error': 'Monitor not found'}, 404 hours = request.args.get('hours', 24, type=int) limit = request.args.get('limit', 200, type=int) checks = MonitorService.get_check_history(monitor_id, hours=hours, limit=limit) return jsonify({'checks': [c.to_dict() for c in checks]}) @monitors_bp.route('//uptime', methods=['GET']) @jwt_required() def get_uptime(monitor_id): """Per-day uptime buckets for the 90-day bar strip. Query param: days.""" monitor = MonitorService.get(monitor_id) if not monitor: return {'error': 'Monitor not found'}, 404 days = min(request.args.get('days', 90, type=int), 365) return jsonify({ 'days': MonitorService.uptime_days(monitor_id, days=days), 'uptime_24h': monitor.uptime_24h, 'uptime_7d': monitor.uptime_7d, 'uptime_30d': monitor.uptime_30d, 'uptime_90d': monitor.uptime_90d, }) # --- Incidents ------------------------------------------------------------- # Incidents are core because the scheduler opens them automatically, with or # without the status-page extension installed. @monitors_bp.route('/incidents', methods=['GET']) @jwt_required() def list_incidents(): """Query params: state (active|resolved|all), limit.""" state = request.args.get('state', 'all') limit = request.args.get('limit', 100, type=int) incidents = MonitorService.list_incidents(state=state, limit=limit) return jsonify({'incidents': [i.to_dict() for i in incidents]}) @monitors_bp.route('/incidents', methods=['POST']) @permission_required('monitoring', 'write') def create_incident(): data = json_object() if not data.get('title'): return {'error': 'Incident title is required'}, 400 incident = MonitorService.create_incident(data.get('page_id'), data) return jsonify(incident.to_dict()), 201 @monitors_bp.route('/incidents/', methods=['PATCH', 'PUT']) @permission_required('monitoring', 'write') def update_incident(incident_id): incident = MonitorService.update_incident(incident_id, json_object()) if not incident: return {'error': 'Incident not found'}, 404 return jsonify(incident.to_dict()) @monitors_bp.route('/incidents/', methods=['DELETE']) @permission_required('monitoring', 'write') def delete_incident(incident_id): if not MonitorService.delete_incident(incident_id): return {'error': 'Incident not found'}, 404 return jsonify({'deleted': True})