mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-27 07:14:08 +09:00
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.
77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
# Bucket: PER-APP read. Per-app bandwidth is scoped to callers who
|
|
# can access the app (can_access_app); the cross-app overview stays panel-wide.
|
|
"""REST surface for per-domain bandwidth accounting.
|
|
|
|
Mounted at /api/v1/bandwidth (registered in app/__init__.py).
|
|
"""
|
|
from flask import Blueprint, jsonify, request
|
|
from app.middleware.rbac import get_current_user
|
|
from app.error_reporting import unexpected_response
|
|
from app.exceptions import NotFoundError
|
|
|
|
from ..middleware.rbac import admin_required, viewer_required
|
|
from ..models import Application
|
|
from ..services.bandwidth_service import BandwidthService
|
|
from ..services.request_metrics_service import RequestMetricsService
|
|
from ..services.resource_grant_service import ResourceGrantService
|
|
from app.api._request import json_object
|
|
|
|
bandwidth_bp = Blueprint('bandwidth', __name__)
|
|
|
|
|
|
@bandwidth_bp.route('/apps', methods=['GET'])
|
|
@viewer_required
|
|
def get_apps_bandwidth():
|
|
"""Month totals + 30-day sparkline series for every app with traffic —
|
|
one call for the Services list."""
|
|
data = BandwidthService.overview(days=30)
|
|
# JSON object keys must be strings.
|
|
return jsonify({'apps': {str(k): v for k, v in data.items()}})
|
|
|
|
|
|
@bandwidth_bp.route('/apps/<int:app_id>', methods=['GET'])
|
|
@viewer_required
|
|
def get_app_bandwidth(app_id):
|
|
"""Full daily series (default 90 days) + current-month total for one app,
|
|
scoped to the app's workspace visibility (foreign caller 404)."""
|
|
app = Application.query_active().filter_by(id=app_id).first()
|
|
user = get_current_user()
|
|
if app is None or not ResourceGrantService.can_access_app(user, app):
|
|
return jsonify({'error': 'Not found'}), 404
|
|
days = request.args.get('days', 90, type=int)
|
|
series = BandwidthService.series(app_id=app_id, days=days)
|
|
return jsonify({
|
|
'app_id': app_id,
|
|
'days': len(series),
|
|
'series': series,
|
|
'month_bytes': BandwidthService.monthly_total(app_id),
|
|
})
|
|
|
|
|
|
@bandwidth_bp.route('/apps/<int:app_id>/requests', methods=['GET'])
|
|
@viewer_required
|
|
def get_app_request_metrics(app_id):
|
|
"""Request rate, status classes, latency percentiles and cache hit ratio
|
|
from the timed access log. ``period``: 1h, 24h, 7d or 30d."""
|
|
# Same 404 for a missing and a foreign app; a bad period is a
|
|
# ValidationError (400). Both go through the global handler.
|
|
app = RequestMetricsService.live_app(app_id)
|
|
if app is None or not ResourceGrantService.can_access_app(get_current_user(), app):
|
|
raise NotFoundError('Not found')
|
|
return jsonify(RequestMetricsService.series(
|
|
app_id, period=request.args.get('period', '24h')))
|
|
|
|
|
|
@bandwidth_bp.route('/aggregate', methods=['POST'])
|
|
@admin_required
|
|
def run_aggregate():
|
|
"""Run the daily aggregation now (optionally for a specific day)."""
|
|
try:
|
|
payload = json_object(optional=True)
|
|
result = BandwidthService.aggregate(day=payload.get('day'))
|
|
return jsonify(result)
|
|
except ValueError:
|
|
return jsonify({'error': 'day must be YYYY-MM-DD'}), 400
|
|
except Exception as exc: # noqa: BLE001 - reported, not swallowed
|
|
return unexpected_response(exc)
|