From 3ead1252f424044b31a31a40da7b1aff15e0f2ca Mon Sep 17 00:00:00 2001 From: Lan Date: Wed, 3 Jun 2026 08:01:55 +0800 Subject: [PATCH] feat: add admin config diagnostics --- apps/admin/services.py | 165 ++++++++++++++++++++++++++++++++++++++++- apps/admin/views.py | 17 ++++- 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/apps/admin/services.py b/apps/admin/services.py index 9744ddc..3b38346 100644 --- a/apps/admin/services.py +++ b/apps/admin/services.py @@ -1649,8 +1649,169 @@ class ConfigService: } FLOAT_FIELDS = {"opacity"} - def get_config(self): - return dict(settings.items()) + def get_config(self, include_diagnostics: bool = True): + config = dict(settings.items()) + if not include_diagnostics: + return config + + diagnostics = self.build_config_diagnostics(config) + return { + **config, + "diagnostics": diagnostics, + "diagnosticItems": diagnostics["items"], + "diagnostic_items": diagnostics["items"], + "diagnosticSummary": diagnostics["summary"], + "diagnostic_summary": diagnostics["summary"], + } + + def build_config_diagnostics(self, config: Optional[dict[str, Any]] = None) -> dict[str, Any]: + config = config or dict(settings.items()) + items: list[dict[str, Any]] = [] + + def add_item( + key: str, + severity: str, + category: str, + field: Optional[str], + priority: int, + count: int = 1, + fields: Optional[list[str]] = None, + ) -> None: + target_fields = fields or ([field] if field else []) + action = { + "type": "field" if field else "section", + "field": field, + "fields": target_fields, + "category": category, + } + items.append( + { + "key": key, + "severity": severity, + "category": category, + "priority": priority, + "count": max(int(count or 0), 0), + "field": field, + "fields": target_fields, + "action": action, + "actionType": action["type"], + "action_type": action["type"], + "targetField": field, + "target_field": field, + } + ) + + admin_token = str(config.get("admin_token") or "") + if admin_token == settings.default_config.get("admin_token"): + add_item("default_admin_password", "danger", "security", "admin_token", 100) + + file_storage = str(config.get("file_storage") or "local").strip().lower() + if file_storage == "s3": + missing_fields = [ + field + for field in ["s3_access_key_id", "s3_secret_access_key", "s3_bucket_name"] + if not str(config.get(field) or "").strip() + ] + if missing_fields: + add_item( + "s3_incomplete", + "danger", + "storage", + missing_fields[0], + 95, + count=len(missing_fields), + fields=missing_fields, + ) + elif file_storage == "webdav": + missing_fields = [ + field + for field in ["webdav_url", "webdav_username", "webdav_password"] + if not str(config.get(field) or "").strip() + ] + if missing_fields: + add_item( + "webdav_incomplete", + "danger", + "storage", + missing_fields[0], + 95, + count=len(missing_fields), + fields=missing_fields, + ) + + if self._to_int(config.get("openUpload")) and self._to_int(config.get("max_save_seconds")) <= 0: + add_item("guest_upload_retention", "warning", "retention", "max_save_seconds", 80) + + if ( + self._to_int(config.get("uploadSize")) >= 50 * 1024 * 1024 + and not self._to_int(config.get("enableChunk")) + ): + add_item("chunking_recommended", "warning", "upload", "enableChunk", 70) + + if self._to_int(config.get("uploadMinute")) <= 0 or self._to_int(config.get("uploadCount")) <= 0: + add_item( + "upload_guard_disabled", + "warning", + "upload", + "uploadMinute", + 60, + fields=["uploadMinute", "uploadCount"], + ) + + if self._to_int(config.get("errorMinute")) <= 0 or self._to_int(config.get("errorCount")) <= 0: + add_item( + "access_guard_disabled", + "warning", + "security", + "errorMinute", + 55, + fields=["errorMinute", "errorCount"], + ) + + expire_style = config.get("expireStyle") + if not isinstance(expire_style, list) or len(expire_style) == 0: + add_item("expiration_style_empty", "danger", "retention", "expireStyle", 75) + + if not items: + add_item("healthy", "success", "system", None, 10, count=0) + + items.sort(key=lambda item: (-item["priority"], item["key"])) + summary = self._build_config_diagnostic_summary(items) + return { + "items": items, + "diagnosticItems": items, + "diagnostic_items": items, + "summary": summary, + "diagnosticSummary": summary, + "diagnostic_summary": summary, + } + + def _build_config_diagnostic_summary(self, items: list[dict[str, Any]]) -> dict[str, Any]: + severity_order = {"danger": 3, "warning": 2, "neutral": 1, "success": 0} + strongest_severity = max( + (item["severity"] for item in items), + key=lambda severity: severity_order.get(severity, 0), + default="success", + ) + return { + "total": len(items), + "dangerCount": sum(1 for item in items if item["severity"] == "danger"), + "danger_count": sum(1 for item in items if item["severity"] == "danger"), + "warningCount": sum(1 for item in items if item["severity"] == "warning"), + "warning_count": sum(1 for item in items if item["severity"] == "warning"), + "successCount": sum(1 for item in items if item["severity"] == "success"), + "success_count": sum(1 for item in items if item["severity"] == "success"), + "neutralCount": sum(1 for item in items if item["severity"] == "neutral"), + "neutral_count": sum(1 for item in items if item["severity"] == "neutral"), + "strongestSeverity": strongest_severity, + "strongest_severity": strongest_severity, + } + + def _to_int(self, value: Any) -> int: + try: + return int(value or 0) + except (TypeError, ValueError): + return 0 async def update_config(self, data: dict): current_config = dict(settings.items()) diff --git a/apps/admin/views.py b/apps/admin/views.py index 44851c1..f8c168f 100644 --- a/apps/admin/views.py +++ b/apps/admin/views.py @@ -494,13 +494,28 @@ async def get_config( return APIResponse(detail=config_service.get_config()) +@admin_api.get("/config/diagnostics") +async def get_config_diagnostics( + config_service: ConfigService = Depends(get_config_service), +): + return APIResponse(detail=config_service.build_config_diagnostics()) + + @admin_api.patch("/config/update") async def update_config( data: dict, config_service: ConfigService = Depends(get_config_service), file_service: FileService = Depends(get_file_service), ): - data.pop("themesChoices", None) + for field in ( + "themesChoices", + "diagnostics", + "diagnosticItems", + "diagnostic_items", + "diagnosticSummary", + "diagnostic_summary", + ): + data.pop(field, None) await config_service.update_config(data) await file_service.record_admin_activity( action="config.update",