mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-02 18:17:11 +09:00
Two dishonesty directions, one rule (plan 75 §A3/A4): a probe answers a value or None, never 0 when undetermined — and a real 0 must survive. Live paths normalized every missing reading to 0 while the history path stored NULL for the same probe: agent counters an LXC agent can't determine rendered as a perfectly idle box; load average on unsupported platforms read 0.00 (and silenced the load alert); CPU freq read 0 MHz; an empty history window summarized as '0.0 avg'. The mobile summary called a method that does not exist and rendered 0/0/0 permanently. One unavailable network counter raised uncaught and froze the entire live broadcast. Reverse direction: falsy traps (x if x else None) collapsed a genuinely idle 0.0 into 'no data' in rollups, averages, and the history model. Fleet: KPI tiles read '0.0 ms latency' / '100% success' with no data; never-probed servers exported serverkit_server_up 0 to external alertmanagers — the up-series now emits only for determined connectivity. get_all_metrics is per-section fault-tolerant so one failed probe can't blank the other six.
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
from flask import Blueprint, request, jsonify
|
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
from app.models.user import User
|
|
from app import db
|
|
|
|
mobile_bp = Blueprint('mobile', __name__)
|
|
|
|
|
|
@mobile_bp.route('/push/register', methods=['POST'])
|
|
@jwt_required()
|
|
def register_push():
|
|
"""Register a device for push notifications."""
|
|
user_id = get_jwt_identity()
|
|
data = request.get_json() or {}
|
|
subscription = data.get('subscription')
|
|
device_name = data.get('device_name', 'Unknown')
|
|
|
|
if not subscription:
|
|
return jsonify({'error': 'subscription required'}), 400
|
|
|
|
user = User.query.get(user_id)
|
|
if not user:
|
|
return jsonify({'error': 'User not found'}), 404
|
|
|
|
# Store push subscription in user metadata
|
|
import json
|
|
push_subs = json.loads(user.push_subscriptions_json) if hasattr(user, 'push_subscriptions_json') and user.push_subscriptions_json else []
|
|
# Avoid duplicates
|
|
existing = next((s for s in push_subs if s.get('endpoint') == subscription.get('endpoint')), None)
|
|
if not existing:
|
|
push_subs.append({
|
|
'subscription': subscription,
|
|
'device_name': device_name,
|
|
'registered_at': __import__('datetime').datetime.utcnow().isoformat(),
|
|
})
|
|
|
|
user.push_subscriptions_json = json.dumps(push_subs)
|
|
db.session.commit()
|
|
|
|
return jsonify({'message': 'Device registered', 'device_count': len(push_subs)})
|
|
|
|
|
|
@mobile_bp.route('/push/unregister', methods=['POST'])
|
|
@jwt_required()
|
|
def unregister_push():
|
|
"""Unregister a device from push notifications."""
|
|
data = request.get_json() or {}
|
|
endpoint = data.get('endpoint')
|
|
if not endpoint:
|
|
return jsonify({'error': 'endpoint required'}), 400
|
|
return jsonify({'message': 'Device unregistered'})
|
|
|
|
|
|
@mobile_bp.route('/quick-actions', methods=['GET'])
|
|
@jwt_required()
|
|
def get_quick_actions():
|
|
"""Get available quick actions for mobile."""
|
|
return jsonify({'actions': [
|
|
{'id': 'restart_service', 'label': 'Restart Service', 'icon': 'refresh', 'params': ['service_name']},
|
|
{'id': 'view_stats', 'label': 'View Server Stats', 'icon': 'chart', 'params': []},
|
|
{'id': 'acknowledge_alert', 'label': 'Acknowledge Alert', 'icon': 'check', 'params': ['alert_id']},
|
|
{'id': 'run_backup', 'label': 'Run Backup', 'icon': 'download', 'params': ['backup_id']},
|
|
{'id': 'toggle_maintenance', 'label': 'Toggle Maintenance', 'icon': 'wrench', 'params': ['component_id']},
|
|
]})
|
|
|
|
|
|
@mobile_bp.route('/quick-actions/<action_id>', methods=['POST'])
|
|
@jwt_required()
|
|
def execute_quick_action(action_id):
|
|
"""Execute a quick action."""
|
|
data = request.get_json() or {}
|
|
|
|
if action_id == 'view_stats':
|
|
# MonitoringService owns the live-metrics probe; ServerMetricsService
|
|
# (history rollups) has no get_current_metrics — calling it here used
|
|
# to raise AttributeError and 500 every time.
|
|
from app.services.monitoring_service import MonitoringService
|
|
metrics = MonitoringService.get_current_metrics()
|
|
return jsonify({'action': 'view_stats', 'result': metrics})
|
|
|
|
elif action_id == 'acknowledge_alert':
|
|
alert_id = data.get('alert_id')
|
|
return jsonify({'action': 'acknowledge_alert', 'alert_id': alert_id, 'acknowledged': True})
|
|
|
|
return jsonify({'action': action_id, 'status': 'executed'})
|
|
|
|
|
|
@mobile_bp.route('/summary', methods=['GET'])
|
|
@jwt_required()
|
|
def get_mobile_summary():
|
|
"""Get a compact summary for mobile dashboard."""
|
|
try:
|
|
from app.services.monitoring_service import MonitoringService
|
|
metrics = MonitoringService.get_current_metrics()
|
|
except Exception:
|
|
metrics = {}
|
|
|
|
from app.models.server import Server
|
|
from app.models.security_alert import SecurityAlert
|
|
|
|
server_count = Server.query.count()
|
|
active_alerts = SecurityAlert.query.filter_by(resolved=False).count() if hasattr(SecurityAlert, 'resolved') else 0
|
|
|
|
# MonitoringService returns nested sections; a section that could not be
|
|
# probed surfaces as None, never 0 (0 would read as a real measurement).
|
|
return jsonify({
|
|
'metrics': {
|
|
'cpu': (metrics.get('cpu') or {}).get('percent'),
|
|
'memory': (metrics.get('memory') or {}).get('percent'),
|
|
'disk': (metrics.get('disk') or {}).get('percent'),
|
|
},
|
|
'servers': server_count,
|
|
'active_alerts': active_alerts,
|
|
})
|
|
|
|
|
|
@mobile_bp.route('/offline-cache', methods=['GET'])
|
|
@jwt_required()
|
|
def get_offline_data():
|
|
"""Get data for offline caching."""
|
|
from app.models.server import Server
|
|
|
|
servers = Server.query.limit(50).all()
|
|
return jsonify({
|
|
'servers': [{'id': s.id, 'name': s.name, 'hostname': s.hostname, 'status': s.status} for s in servers],
|
|
'cached_at': __import__('datetime').datetime.utcnow().isoformat(),
|
|
})
|