Files
ServerKit/backend/app/api/mobile.py
T
Juan Denis 55d8fb1e44 Read route bodies through json_object()
328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.

Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
2026-10-10 20:53:43 -04:00

130 lines
4.8 KiB
Python

from flask import Blueprint, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.models.user import User
from app import db
from app.exceptions import not_found
from app.api._request import json_object
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 = json_object()
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:
raise not_found('user')
# 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 = json_object()
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 = json_object()
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(),
})