Finalize diagnostics, logging controls, and email test support
This commit is contained in:
@@ -84,9 +84,11 @@ from ..services.invite_email import (
|
||||
get_invite_email_templates,
|
||||
reset_invite_email_template,
|
||||
save_invite_email_template,
|
||||
send_test_email,
|
||||
send_templated_email,
|
||||
smtp_email_config_ready,
|
||||
)
|
||||
from ..services.diagnostics import get_diagnostics_catalog, run_diagnostics
|
||||
import logging
|
||||
from ..logging_config import configure_logging
|
||||
from ..routers import requests as requests_router
|
||||
@@ -192,6 +194,10 @@ SETTING_KEYS: List[str] = [
|
||||
"qbittorrent_password",
|
||||
"log_level",
|
||||
"log_file",
|
||||
"log_file_max_bytes",
|
||||
"log_file_backup_count",
|
||||
"log_http_client_level",
|
||||
"log_background_sync_level",
|
||||
"requests_sync_ttl_minutes",
|
||||
"requests_poll_interval_seconds",
|
||||
"requests_delta_sync_interval_minutes",
|
||||
@@ -609,6 +615,7 @@ async def list_settings() -> Dict[str, Any]:
|
||||
async def update_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
updates = 0
|
||||
touched_logging = False
|
||||
changed_keys: List[str] = []
|
||||
for key, value in payload.items():
|
||||
if key not in SETTING_KEYS:
|
||||
raise HTTPException(status_code=400, detail=f"Unknown setting: {key}")
|
||||
@@ -617,6 +624,7 @@ async def update_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if isinstance(value, str) and value.strip() == "":
|
||||
delete_setting(key)
|
||||
updates += 1
|
||||
changed_keys.append(key)
|
||||
continue
|
||||
value_to_store = str(value).strip() if isinstance(value, str) else str(value)
|
||||
if key in URL_SETTING_KEYS and value_to_store:
|
||||
@@ -627,14 +635,79 @@ async def update_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
raise HTTPException(status_code=400, detail=f"{friendly_key}: {exc}") from exc
|
||||
set_setting(key, value_to_store)
|
||||
updates += 1
|
||||
if key in {"log_level", "log_file"}:
|
||||
changed_keys.append(key)
|
||||
if key in {"log_level", "log_file", "log_file_max_bytes", "log_file_backup_count", "log_http_client_level", "log_background_sync_level"}:
|
||||
touched_logging = True
|
||||
if touched_logging:
|
||||
runtime = get_runtime_settings()
|
||||
configure_logging(runtime.log_level, runtime.log_file)
|
||||
configure_logging(
|
||||
runtime.log_level,
|
||||
runtime.log_file,
|
||||
log_file_max_bytes=runtime.log_file_max_bytes,
|
||||
log_file_backup_count=runtime.log_file_backup_count,
|
||||
log_http_client_level=runtime.log_http_client_level,
|
||||
log_background_sync_level=runtime.log_background_sync_level,
|
||||
)
|
||||
logger.info("Admin updated settings: count=%s keys=%s", updates, changed_keys)
|
||||
return {"status": "ok", "updated": updates}
|
||||
|
||||
|
||||
@router.post("/settings/test/email")
|
||||
async def test_email_settings(request: Request) -> Dict[str, Any]:
|
||||
recipient_email = None
|
||||
content_type = (request.headers.get("content-type") or "").split(";", 1)[0].strip().lower()
|
||||
try:
|
||||
if content_type == "application/json":
|
||||
payload = await request.json()
|
||||
if isinstance(payload, dict) and isinstance(payload.get("recipient_email"), str):
|
||||
recipient_email = payload["recipient_email"]
|
||||
elif content_type in {
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
}:
|
||||
form = await request.form()
|
||||
candidate = form.get("recipient_email")
|
||||
if isinstance(candidate, str):
|
||||
recipient_email = candidate
|
||||
except Exception:
|
||||
recipient_email = None
|
||||
try:
|
||||
result = await send_test_email(recipient_email=recipient_email)
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
logger.info("Admin triggered SMTP test: recipient=%s", result.get("recipient_email"))
|
||||
return {"status": "ok", **result}
|
||||
|
||||
|
||||
@router.get("/diagnostics")
|
||||
async def diagnostics_catalog() -> Dict[str, Any]:
|
||||
return {"status": "ok", **get_diagnostics_catalog()}
|
||||
|
||||
|
||||
@router.post("/diagnostics/run")
|
||||
async def diagnostics_run(payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
keys: Optional[List[str]] = None
|
||||
recipient_email: Optional[str] = None
|
||||
if payload is not None:
|
||||
raw_keys = payload.get("keys")
|
||||
if raw_keys is not None:
|
||||
if not isinstance(raw_keys, list):
|
||||
raise HTTPException(status_code=400, detail="keys must be an array of diagnostic keys")
|
||||
keys = []
|
||||
for raw_key in raw_keys:
|
||||
if not isinstance(raw_key, str):
|
||||
raise HTTPException(status_code=400, detail="Each diagnostic key must be a string")
|
||||
normalized = raw_key.strip()
|
||||
if normalized:
|
||||
keys.append(normalized)
|
||||
raw_recipient_email = payload.get("recipient_email")
|
||||
if raw_recipient_email is not None:
|
||||
if not isinstance(raw_recipient_email, str):
|
||||
raise HTTPException(status_code=400, detail="recipient_email must be a string")
|
||||
recipient_email = raw_recipient_email.strip() or None
|
||||
return {"status": "ok", **(await run_diagnostics(keys, recipient_email=recipient_email))}
|
||||
|
||||
|
||||
@router.get("/sonarr/options")
|
||||
async def sonarr_options() -> Dict[str, Any]:
|
||||
runtime = get_runtime_settings()
|
||||
@@ -1061,12 +1134,14 @@ async def get_user_summary_by_id(user_id: int) -> Dict[str, Any]:
|
||||
@router.post("/users/{username}/block")
|
||||
async def block_user(username: str) -> Dict[str, Any]:
|
||||
set_user_blocked(username, True)
|
||||
logger.warning("Admin blocked user: username=%s", username)
|
||||
return {"status": "ok", "username": username, "blocked": True}
|
||||
|
||||
|
||||
@router.post("/users/{username}/unblock")
|
||||
async def unblock_user(username: str) -> Dict[str, Any]:
|
||||
set_user_blocked(username, False)
|
||||
logger.info("Admin unblocked user: username=%s", username)
|
||||
return {"status": "ok", "username": username, "blocked": False}
|
||||
|
||||
|
||||
@@ -1173,6 +1248,17 @@ async def user_system_action(username: str, payload: Dict[str, Any]) -> Dict[str
|
||||
for system in (result.get("jellyfin"), result.get("jellyseerr"), result.get("email"))
|
||||
):
|
||||
result["status"] = "partial"
|
||||
logger.info(
|
||||
"Admin system action completed: username=%s action=%s overall=%s local=%s jellyfin=%s jellyseerr=%s invites=%s email=%s",
|
||||
username,
|
||||
action,
|
||||
result.get("status"),
|
||||
result.get("local", {}).get("status"),
|
||||
result.get("jellyfin", {}).get("status"),
|
||||
result.get("jellyseerr", {}).get("status"),
|
||||
result.get("invites", {}).get("status"),
|
||||
result.get("email", {}).get("status"),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -1450,6 +1536,15 @@ async def create_profile(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
)
|
||||
except sqlite3.IntegrityError as exc:
|
||||
raise HTTPException(status_code=409, detail="A profile with that name already exists") from exc
|
||||
logger.info(
|
||||
"Admin created profile: profile_id=%s name=%s role=%s active=%s auto_search=%s expires_days=%s",
|
||||
profile.get("id"),
|
||||
profile.get("name"),
|
||||
profile.get("role"),
|
||||
profile.get("is_active"),
|
||||
profile.get("auto_search_enabled"),
|
||||
profile.get("account_expires_days"),
|
||||
)
|
||||
return {"status": "ok", "profile": profile}
|
||||
|
||||
|
||||
@@ -1487,6 +1582,15 @@ async def edit_profile(profile_id: int, payload: Dict[str, Any]) -> Dict[str, An
|
||||
raise HTTPException(status_code=409, detail="A profile with that name already exists") from exc
|
||||
if not profile:
|
||||
raise HTTPException(status_code=404, detail="Profile not found")
|
||||
logger.info(
|
||||
"Admin updated profile: profile_id=%s name=%s role=%s active=%s auto_search=%s expires_days=%s",
|
||||
profile.get("id"),
|
||||
profile.get("name"),
|
||||
profile.get("role"),
|
||||
profile.get("is_active"),
|
||||
profile.get("auto_search_enabled"),
|
||||
profile.get("account_expires_days"),
|
||||
)
|
||||
return {"status": "ok", "profile": profile}
|
||||
|
||||
|
||||
@@ -1498,6 +1602,7 @@ async def remove_profile(profile_id: int) -> Dict[str, Any]:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Profile not found")
|
||||
logger.warning("Admin deleted profile: profile_id=%s", profile_id)
|
||||
return {"status": "ok", "deleted": True, "profile_id": profile_id}
|
||||
|
||||
|
||||
@@ -1561,6 +1666,7 @@ async def update_invite_policy(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
master_invite_value = payload.get("master_invite_id")
|
||||
if master_invite_value in (None, "", 0, "0"):
|
||||
set_setting(SELF_SERVICE_INVITE_MASTER_ID_KEY, None)
|
||||
logger.info("Admin cleared invite policy master invite")
|
||||
return {"status": "ok", "policy": {"master_invite_id": None, "master_invite": None}}
|
||||
try:
|
||||
master_invite_id = int(master_invite_value)
|
||||
@@ -1572,6 +1678,7 @@ async def update_invite_policy(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not invite:
|
||||
raise HTTPException(status_code=404, detail="Master invite not found")
|
||||
set_setting(SELF_SERVICE_INVITE_MASTER_ID_KEY, str(master_invite_id))
|
||||
logger.info("Admin updated invite policy: master_invite_id=%s", master_invite_id)
|
||||
return {
|
||||
"status": "ok",
|
||||
"policy": {
|
||||
@@ -1613,6 +1720,7 @@ async def update_invite_email_template_settings(template_key: str, payload: Dict
|
||||
body_text=body_text or "",
|
||||
body_html=body_html or "",
|
||||
)
|
||||
logger.info("Admin updated invite email template: template=%s", template_key)
|
||||
return {"status": "ok", "template": template}
|
||||
|
||||
|
||||
@@ -1621,6 +1729,7 @@ async def reset_invite_email_template_settings(template_key: str) -> Dict[str, A
|
||||
if template_key not in INVITE_EMAIL_TEMPLATE_KEYS:
|
||||
raise HTTPException(status_code=404, detail="Email template not found")
|
||||
template = reset_invite_email_template(template_key)
|
||||
logger.info("Admin reset invite email template: template=%s", template_key)
|
||||
return {"status": "ok", "template": template}
|
||||
|
||||
|
||||
@@ -1666,6 +1775,13 @@ async def send_invite_email(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
logger.info(
|
||||
"Admin sent invite email template: template=%s recipient=%s invite_id=%s username=%s",
|
||||
template_key,
|
||||
result.get("recipient_email"),
|
||||
invite.get("id") if invite else None,
|
||||
user.get("username") if user else None,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
@@ -1725,6 +1841,18 @@ async def create_invite(payload: Dict[str, Any], current_user: Dict[str, Any] =
|
||||
)
|
||||
except Exception as exc:
|
||||
email_error = str(exc)
|
||||
logger.info(
|
||||
"Admin created invite: invite_id=%s code=%s label=%s profile_id=%s role=%s max_uses=%s enabled=%s recipient_email=%s send_email=%s",
|
||||
invite.get("id"),
|
||||
invite.get("code"),
|
||||
invite.get("label"),
|
||||
invite.get("profile_id"),
|
||||
invite.get("role"),
|
||||
invite.get("max_uses"),
|
||||
invite.get("enabled"),
|
||||
invite.get("recipient_email"),
|
||||
send_email,
|
||||
)
|
||||
return {
|
||||
"status": "partial" if email_error else "ok",
|
||||
"invite": invite,
|
||||
@@ -1785,6 +1913,18 @@ async def edit_invite(invite_id: int, payload: Dict[str, Any]) -> Dict[str, Any]
|
||||
)
|
||||
except Exception as exc:
|
||||
email_error = str(exc)
|
||||
logger.info(
|
||||
"Admin updated invite: invite_id=%s code=%s label=%s profile_id=%s role=%s max_uses=%s enabled=%s recipient_email=%s send_email=%s",
|
||||
invite.get("id"),
|
||||
invite.get("code"),
|
||||
invite.get("label"),
|
||||
invite.get("profile_id"),
|
||||
invite.get("role"),
|
||||
invite.get("max_uses"),
|
||||
invite.get("enabled"),
|
||||
invite.get("recipient_email"),
|
||||
send_email,
|
||||
)
|
||||
return {
|
||||
"status": "partial" if email_error else "ok",
|
||||
"invite": invite,
|
||||
@@ -1803,4 +1943,5 @@ async def remove_invite(invite_id: int) -> Dict[str, Any]:
|
||||
deleted = delete_signup_invite(invite_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Invite not found")
|
||||
logger.warning("Admin deleted invite: invite_id=%s", invite_id)
|
||||
return {"status": "ok", "deleted": True, "invite_id": invite_id}
|
||||
|
||||
Reference in New Issue
Block a user