mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-10 05:53:05 +09:00
Daily read-only drift sweep re-renders expected nginx vhosts and compose overrides from panel state (via extracted pure render seams) and diffs against disk; drift notifies admins. Doctor tab on Monitoring runs the full diagnosis (drift, services, certs, disk, DB) with diff-confirmed per-item and batch repair. Nothing auto-repairs. The nginx render seam also carries the micro-cache blocks used by the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
105 lines
4.0 KiB
Python
105 lines
4.0 KiB
Python
"""REST surface for the doctor sweep + configuration drift.
|
|
|
|
Mounted at /api/v1/doctor (registered in app/__init__.py). Admin-only.
|
|
|
|
Contract (the CLI codes against this too):
|
|
GET /drift -> {'report': <last drift report>|null}
|
|
POST /drift/check -> 202 {'job_id'}
|
|
POST /drift/<type>/<id>/repair -> repair result; body must carry
|
|
{'confirm': true} (400 otherwise)
|
|
GET / -> {'report': <last doctor report>|null}
|
|
POST /run -> runs synchronously, {'report': ...}
|
|
POST /repair -> {'results': [...]} for body
|
|
{'items': [{kind, type?, id?, name?}]}
|
|
"""
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
from ..middleware.rbac import admin_required
|
|
from ..services.doctor_service import DoctorService
|
|
from ..services.drift_service import DRIFT_JOB_KIND, DriftService
|
|
|
|
doctor_bp = Blueprint('doctor', __name__)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Drift
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
@doctor_bp.route('/drift', methods=['GET'])
|
|
@admin_required
|
|
def get_drift_report():
|
|
"""Last stored drift report (null when no sweep has run yet)."""
|
|
try:
|
|
return jsonify({'report': DriftService.get_last_report()})
|
|
except Exception as exc: # noqa: BLE001 — surface as a clean JSON error
|
|
return jsonify({'error': str(exc)}), 500
|
|
|
|
|
|
@doctor_bp.route('/drift/check', methods=['POST'])
|
|
@admin_required
|
|
def run_drift_check():
|
|
"""Enqueue a one-off drift sweep job."""
|
|
try:
|
|
from app.jobs.service import JobService
|
|
job = JobService.enqueue(DRIFT_JOB_KIND, payload={}, max_attempts=1)
|
|
return jsonify({'job_id': job.id}), 202
|
|
except Exception as exc: # noqa: BLE001
|
|
return jsonify({'error': str(exc)}), 500
|
|
|
|
|
|
@doctor_bp.route('/drift/<check_type>/<resource_id>/repair', methods=['POST'])
|
|
@admin_required
|
|
def repair_drift(check_type, resource_id):
|
|
"""Repair one drifted resource. Explicit confirmation required."""
|
|
data = request.get_json(silent=True) or {}
|
|
if data.get('confirm') is not True:
|
|
return jsonify({'error': 'Confirmation required: pass {"confirm": true}.'}), 400
|
|
try:
|
|
rid = int(resource_id) if resource_id.isdigit() else resource_id
|
|
result = DriftService.repair(check_type, rid)
|
|
status = 200 if result.get('success') else 400
|
|
return jsonify(result), status
|
|
except Exception as exc: # noqa: BLE001
|
|
return jsonify({'error': str(exc)}), 500
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Doctor
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
@doctor_bp.route('', methods=['GET'])
|
|
@doctor_bp.route('/', methods=['GET'])
|
|
@admin_required
|
|
def get_doctor_report():
|
|
"""Last stored doctor report (null when the doctor has never run)."""
|
|
try:
|
|
return jsonify({'report': DoctorService.get_last_report()})
|
|
except Exception as exc: # noqa: BLE001
|
|
return jsonify({'error': str(exc)}), 500
|
|
|
|
|
|
@doctor_bp.route('/run', methods=['POST'])
|
|
@admin_required
|
|
def run_doctor():
|
|
"""Run the sweep synchronously (the doctor is interactive; every internal
|
|
probe is time-capped) and return the fresh report."""
|
|
try:
|
|
report = DoctorService.run()
|
|
return jsonify({'report': report})
|
|
except Exception as exc: # noqa: BLE001
|
|
return jsonify({'error': str(exc)}), 500
|
|
|
|
|
|
@doctor_bp.route('/repair', methods=['POST'])
|
|
@admin_required
|
|
def repair_items():
|
|
"""Batch-repair the explicit items the operator selected."""
|
|
data = request.get_json(silent=True) or {}
|
|
items = data.get('items')
|
|
if not isinstance(items, list) or not items:
|
|
return jsonify({'error': "Body must carry a non-empty 'items' list."}), 400
|
|
try:
|
|
return jsonify({'results': DoctorService.repair(items)})
|
|
except Exception as exc: # noqa: BLE001
|
|
return jsonify({'error': str(exc)}), 500
|