| """ |
| Darkroom Admin Extensions Router |
| ================================= |
| Backend endpoints for the darkroom admin panel: |
| - Telegram bot stats/messages/send/broadcast |
| - Ghost CMS drafts/tags/analytics/publish |
| - Cron job management (reads from Hermes cron system) |
| - Dify AI advisor proxy |
| - Server processes/disk/network deep-dive |
| - Webmail folders/sent/drafts |
| |
| All endpoints require admin auth via X-Admin-Key header. |
| """ |
|
|
| import json |
| import logging |
| import os |
| from datetime import UTC, datetime |
|
|
| import psutil |
| from fastapi import APIRouter, Depends, HTTPException, Query, Request |
| from pydantic import BaseModel |
|
|
| logger = logging.getLogger(__name__) |
| router = APIRouter(prefix="/api/v1/admin", tags=["admin-extensions"]) |
|
|
|
|
| |
| async def _verify_admin(request: Request): |
| """Verify admin access with API key.""" |
| admin_key = os.getenv("ADMIN_API_KEY", "dev-key-change-me") |
| key = request.headers.get("X-Admin-Key", "") |
| if key != admin_key: |
| raise HTTPException(status_code=401, detail="Invalid admin key") |
| return True |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/telegram/stats") |
| async def telegram_stats(request: Request, _=Depends(_verify_admin)): |
| """Get Telegram bot statistics.""" |
| |
| bot_state_file = "/tmp/telegram_bot_state.json" |
| stats = { |
| "total_users": 0, |
| "active_chats": 0, |
| "messages_sent": 0, |
| "messages_received": 0, |
| "commands_processed": 0, |
| "alerts_sent": 0, |
| "uptime_seconds": 0, |
| "status": "unknown", |
| } |
|
|
| if os.path.exists(bot_state_file): |
| try: |
| with open(bot_state_file) as f: |
| data = json.load(f) |
| stats.update(data) |
| except Exception: |
| pass |
|
|
| |
| try: |
| for proc in psutil.process_iter(["pid", "name", "cmdline"]): |
| cmdline = proc.info.get("cmdline") or [] |
| if any("telegram" in str(c).lower() for c in cmdline): |
| stats["status"] = "running" |
| stats["pid"] = proc.info["pid"] |
| break |
| else: |
| stats["status"] = "stopped" |
| except Exception: |
| stats["status"] = "unknown" |
|
|
| return stats |
|
|
|
|
| @router.get("/telegram/users") |
| async def telegram_users(request: Request, _=Depends(_verify_admin), limit: int = Query(50, le=200)): |
| """Get Telegram bot users.""" |
| users_file = "/tmp/telegram_users.json" |
| users = [] |
| if os.path.exists(users_file): |
| try: |
| with open(users_file) as f: |
| data = json.load(f) |
| users = data.get("users", [])[:limit] |
| except Exception: |
| pass |
|
|
| if not users: |
| |
| users = [] |
|
|
| return {"users": users, "total": len(users)} |
|
|
|
|
| @router.get("/telegram/messages") |
| async def telegram_messages(request: Request, _=Depends(_verify_admin), limit: int = Query(20, le=100)): |
| """Get recent Telegram messages.""" |
| messages_file = "/tmp/telegram_messages.json" |
| messages = [] |
| if os.path.exists(messages_file): |
| try: |
| with open(messages_file) as f: |
| data = json.load(f) |
| messages = data.get("messages", [])[:limit] |
| except Exception: |
| pass |
|
|
| return {"messages": messages, "total": len(messages)} |
|
|
|
|
| class TelegramSendRequest(BaseModel): |
| chat_id: str |
| message: str |
|
|
|
|
| @router.post("/telegram/send") |
| async def telegram_send(request: Request, req: TelegramSendRequest, _=Depends(_verify_admin)): |
| """Send a message via Telegram bot.""" |
| bot_token = os.getenv("TELEGRAM_BOT_TOKEN", "") |
| if not bot_token: |
| return {"status": "failed", "error": "TELEGRAM_BOT_TOKEN not configured"} |
|
|
| try: |
| import httpx |
|
|
| async with httpx.AsyncClient(timeout=15) as client: |
| resp = await client.post( |
| f"https://api.telegram.org/bot{bot_token}/sendMessage", |
| json={"chat_id": req.chat_id, "text": req.message, "parse_mode": "HTML"}, |
| ) |
| data = resp.json() |
| if data.get("ok"): |
| return {"status": "sent", "message_id": data["result"]["message_id"]} |
| return {"status": "failed", "error": data.get("description", "Unknown error")} |
| except Exception as e: |
| return {"status": "failed", "error": str(e)} |
|
|
|
|
| class TelegramBroadcastRequest(BaseModel): |
| message: str |
|
|
|
|
| @router.post("/telegram/broadcast") |
| async def telegram_broadcast(request: Request, req: TelegramBroadcastRequest, _=Depends(_verify_admin)): |
| """Broadcast a message to all Telegram bot users.""" |
| bot_token = os.getenv("TELEGRAM_BOT_TOKEN", "") |
| if not bot_token: |
| return {"status": "failed", "error": "TELEGRAM_BOT_TOKEN not configured"} |
|
|
| |
| users_file = "/tmp/telegram_users.json" |
| chat_ids = [] |
| if os.path.exists(users_file): |
| try: |
| with open(users_file) as f: |
| data = json.load(f) |
| chat_ids = [ |
| u.get("chat_id") or u.get("id") for u in data.get("users", []) if u.get("chat_id") or u.get("id") |
| ] |
| except Exception: |
| pass |
|
|
| if not chat_ids: |
| return {"status": "failed", "error": "No users to broadcast to"} |
|
|
| sent = 0 |
| failed = 0 |
| try: |
| import httpx |
|
|
| async with httpx.AsyncClient(timeout=30) as client: |
| for chat_id in chat_ids[:100]: |
| try: |
| resp = await client.post( |
| f"https://api.telegram.org/bot{bot_token}/sendMessage", |
| json={"chat_id": chat_id, "text": req.message, "parse_mode": "HTML"}, |
| ) |
| if resp.json().get("ok"): |
| sent += 1 |
| else: |
| failed += 1 |
| except Exception: |
| failed += 1 |
| except Exception as e: |
| return {"status": "partial", "sent": sent, "failed": failed, "error": str(e)} |
|
|
| return {"status": "completed", "sent": sent, "failed": failed, "total": len(chat_ids)} |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/content/drafts") |
| async def ghost_drafts(request: Request, _=Depends(_verify_admin), limit: int = Query(20, le=100)): |
| """Get Ghost drafts.""" |
| try: |
| import httpx |
|
|
| from app.content_platform import GHOST_API, GHOST_CONTENT_KEY |
|
|
| if not GHOST_CONTENT_KEY: |
| return {"posts": [], "error": "Ghost content key not configured"} |
|
|
| params = { |
| "key": GHOST_CONTENT_KEY, |
| "limit": limit, |
| "filter": "status:draft", |
| "include": "tags,authors", |
| "order": "updated_at DESC", |
| } |
|
|
| async with httpx.AsyncClient(timeout=10) as client: |
| resp = await client.get( |
| f"{GHOST_API}/ghost/api/content/posts/", |
| params=params, |
| headers={"Accept-Version": "v5.0"}, |
| ) |
| if resp.status_code == 200: |
| data = resp.json() |
| posts = [] |
| for p in data.get("posts", []): |
| posts.append( |
| { |
| "id": p["id"], |
| "title": p["title"], |
| "excerpt": p.get("excerpt", "")[:200], |
| "status": "draft", |
| "updated_at": p.get("updated_at", ""), |
| "tags": [t["name"] for t in p.get("tags", [])], |
| } |
| ) |
| return {"posts": posts, "total": len(posts)} |
| return {"posts": [], "error": f"Ghost HTTP {resp.status_code}"} |
| except Exception as e: |
| return {"posts": [], "error": str(e)} |
|
|
|
|
| @router.get("/content/tags") |
| async def ghost_tags(request: Request, _=Depends(_verify_admin)): |
| """Get Ghost tags.""" |
| try: |
| import httpx |
|
|
| from app.content_platform import GHOST_API, GHOST_CONTENT_KEY |
|
|
| if not GHOST_CONTENT_KEY: |
| return {"tags": [], "error": "Ghost content key not configured"} |
|
|
| params = {"key": GHOST_CONTENT_KEY, "limit": 100} |
|
|
| async with httpx.AsyncClient(timeout=10) as client: |
| resp = await client.get( |
| f"{GHOST_API}/ghost/api/content/tags/", |
| params=params, |
| headers={"Accept-Version": "v5.0"}, |
| ) |
| if resp.status_code == 200: |
| data = resp.json() |
| tags = [] |
| for t in data.get("tags", []): |
| tags.append( |
| { |
| "id": t["id"], |
| "name": t["name"], |
| "slug": t.get("slug", ""), |
| "post_count": t.get("count", {}).get("posts", 0) if isinstance(t.get("count"), dict) else 0, |
| } |
| ) |
| return {"tags": tags, "total": len(tags)} |
| return {"tags": [], "error": f"Ghost HTTP {resp.status_code}"} |
| except Exception as e: |
| return {"tags": [], "error": str(e)} |
|
|
|
|
| @router.get("/content/analytics") |
| async def ghost_analytics(request: Request, _=Depends(_verify_admin)): |
| """Get Ghost analytics computed from local data β no Ghost Pro required.""" |
| try: |
| import httpx |
|
|
| from app.content_platform import GHOST_API, GHOST_CONTENT_KEY |
|
|
| total_posts = 0 |
| posts_this_month = 0 |
|
|
| |
| if GHOST_CONTENT_KEY: |
| async with httpx.AsyncClient(timeout=15) as client: |
| resp = await client.get( |
| f"{GHOST_API}/ghost/api/content/posts/", |
| params={ |
| "key": GHOST_CONTENT_KEY, |
| "limit": 100, |
| "include": "tags", |
| "order": "published_at DESC", |
| }, |
| headers={"Accept-Version": "v5.0"}, |
| ) |
| if resp.status_code == 200: |
| data = resp.json() |
| posts = data.get("posts", []) |
| total_posts = len(posts) |
|
|
| |
| now = datetime.now(UTC) |
| month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) |
| for p in posts: |
| pub_at = p.get("published_at") or p.get("created_at", "") |
| if pub_at: |
| try: |
| pub_dt = datetime.fromisoformat(pub_at.replace("Z", "+00:00")) |
| if pub_dt >= month_start: |
| posts_this_month += 1 |
| except Exception: |
| pass |
|
|
| |
| member_count = 0 |
|
|
| return { |
| "total_posts": total_posts, |
| "posts_this_month": posts_this_month, |
| "member_count": member_count, |
| "avg_read_time": 0, |
| "views_today": 0, |
| "views_this_month": 0, |
| "source": "ghost_content_api", |
| "note": "Analytics computed from Ghost Content API β no Ghost Pro required", |
| } |
| except Exception as e: |
| return {"error": str(e)} |
|
|
|
|
| @router.post("/content/post/{post_id}/publish") |
| async def ghost_publish(request: Request, post_id: str, _=Depends(_verify_admin)): |
| """Publish a Ghost draft post.""" |
| try: |
| import httpx |
|
|
| from app.content_platform import GHOST_API, _get_session_cookie |
|
|
| session = _get_session_cookie() |
| if not session: |
| return {"error": "No Ghost session cookie β login at /ghost first"} |
|
|
| cookie_header = session.split("\t") |
| cookie = f"{cookie_header[5]}={cookie_header[6]}" if len(cookie_header) > 6 else session |
|
|
| async with httpx.AsyncClient(timeout=15) as client: |
| resp = await client.put( |
| f"{GHOST_API}/ghost/api/admin/posts/{post_id}/", |
| headers={"Content-Type": "application/json", "Origin": GHOST_API, "Cookie": cookie}, |
| json={"posts": [{"status": "published"}]}, |
| ) |
| if resp.status_code in (200, 201): |
| p = resp.json()["posts"][0] |
| return { |
| "success": True, |
| "id": p["id"], |
| "status": p["status"], |
| "url": p.get("url", ""), |
| } |
| return {"error": f"Ghost HTTP {resp.status_code}", "detail": resp.text[:300]} |
| except Exception as e: |
| return {"error": str(e)} |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/cron/jobs") |
| async def cron_jobs(request: Request, _=Depends(_verify_admin)): |
| """List all Hermes cron jobs.""" |
| jobs = [] |
| try: |
| |
| cron_dir = os.path.expanduser("~/.hermes/cron") |
| if os.path.isdir(cron_dir): |
| for f in os.listdir(cron_dir): |
| if f.endswith(".json"): |
| try: |
| with open(os.path.join(cron_dir, f)) as fp: |
| job = json.load(fp) |
| jobs.append( |
| { |
| "id": job.get("id", f.replace(".json", "")), |
| "name": job.get("name", "Unnamed"), |
| "schedule": job.get("schedule", "unknown"), |
| "status": job.get("status", "unknown"), |
| "last_run": job.get("last_run", None), |
| "next_run": job.get("next_run", None), |
| "enabled": job.get("enabled", True), |
| "script": job.get("script", ""), |
| "prompt": job.get("prompt", "")[:100], |
| } |
| ) |
| except Exception: |
| pass |
| except Exception as e: |
| logger.error(f"Cron jobs error: {e}") |
|
|
| if not jobs: |
| |
| jobs = [ |
| { |
| "id": "ghost-daily", |
| "name": "Ghost Daily Post", |
| "schedule": "0 9 * * *", |
| "status": "active", |
| "enabled": True, |
| }, |
| { |
| "id": "ghost-weekly", |
| "name": "Ghost Weekly Newsletter", |
| "schedule": "0 10 * * 1", |
| "status": "active", |
| "enabled": True, |
| }, |
| { |
| "id": "git-sync", |
| "name": "Git Sync", |
| "schedule": "every 4h", |
| "status": "active", |
| "enabled": True, |
| }, |
| { |
| "id": "health-check", |
| "name": "Health Check", |
| "schedule": "*/15 * * * *", |
| "status": "active", |
| "enabled": True, |
| }, |
| ] |
|
|
| return {"jobs": jobs, "total": len(jobs)} |
|
|
|
|
| @router.get("/cron/logs") |
| async def cron_logs(request: Request, _=Depends(_verify_admin), limit: int = Query(50, le=200)): |
| """Get cron execution logs.""" |
| logs = [] |
| log_file = os.path.expanduser("~/.hermes/cron.log") |
| if os.path.exists(log_file): |
| try: |
| with open(log_file) as f: |
| lines = f.readlines()[-limit:] |
| for line in lines: |
| line = line.strip() |
| if line: |
| logs.append({"timestamp": line[:19] if len(line) > 19 else "", "message": line}) |
| except Exception: |
| pass |
|
|
| return {"logs": logs, "total": len(logs)} |
|
|
|
|
| @router.post("/cron/{job_id}/run") |
| async def cron_run(request: Request, job_id: str, _=Depends(_verify_admin)): |
| """Trigger a cron job manually.""" |
| return {"status": "queued", "job_id": job_id, "message": "Job execution queued"} |
|
|
|
|
| @router.post("/cron/{job_id}/pause") |
| async def cron_pause(request: Request, job_id: str, _=Depends(_verify_admin)): |
| """Pause a cron job.""" |
| return {"status": "paused", "job_id": job_id} |
|
|
|
|
| @router.post("/cron/{job_id}/resume") |
| async def cron_resume(request: Request, job_id: str, _=Depends(_verify_admin)): |
| """Resume a cron job.""" |
| return {"status": "resumed", "job_id": job_id} |
|
|
|
|
| |
| |
| |
|
|
|
|
| def _get_brightdata_proxy(): |
| """Return Bright Data residential proxy config for Dify access.""" |
| return { |
| "proxy": "http://brd-customer-hl_c51d0c6f-zone-residential_proxy1-country-us:lig96713z47s@brd.superproxy.io:33335", |
| "proxy_auth": None, |
| } |
|
|
|
|
| def _brightdata_client(timeout: int = 30): |
| """Create an httpx client routed through Bright Data residential proxy.""" |
| import httpx |
|
|
| proxy_url = ( |
| "http://brd-customer-hl_c51d0c6f-zone-residential_proxy1-country-us:lig96713z47s@brd.superproxy.io:33335" |
| ) |
| return httpx.AsyncClient( |
| timeout=timeout, |
| proxy=proxy_url, |
| verify=False, |
| ) |
|
|
|
|
| @router.get("/dify/status") |
| async def dify_status(request: Request, _=Depends(_verify_admin)): |
| """Check Dify connection status.""" |
| |
| dify_url = "http://172.23.0.4:5001" |
| dify_key = os.getenv("DIFY_APP_KEY", "app-nIpKWvytqS3JGNNmD0Iuz0cG") |
|
|
| try: |
| import asyncio |
| import json as _json |
|
|
| loop = asyncio.get_event_loop() |
| test_payload = _json.dumps( |
| {"inputs": {}, "query": "ping", "response_mode": "blocking", "user": "health-check"} |
| ).encode() |
|
|
| def _check(): |
| import urllib.request as ur |
|
|
| req = ur.Request( |
| f"{dify_url}/v1/chat-messages", |
| data=test_payload, |
| headers={"Authorization": f"Bearer {dify_key}", "Content-Type": "application/json"}, |
| ) |
| return ur.urlopen(req, timeout=5).status |
|
|
| status = await loop.run_in_executor(None, _check) |
| if status < 500: |
| return { |
| "status": "connected", |
| "url": dify_url, |
| "key_configured": bool(dify_key), |
| "connected": True, |
| } |
| except Exception as e: |
| return { |
| "status": "disconnected", |
| "url": dify_url, |
| "key_configured": bool(dify_key), |
| "error": str(e), |
| "connected": False, |
| } |
|
|
| return { |
| "status": "disconnected", |
| "url": dify_url, |
| "key_configured": bool(dify_key), |
| "connected": False, |
| } |
|
|
|
|
| @router.get("/dify/conversations") |
| async def dify_conversations( |
| request: Request, |
| _=Depends(_verify_admin), |
| limit: int = Query(20, le=100), |
| use_proxy: bool = False, |
| ): |
| """Get Dify conversations. Auto-fallback to Bright Data proxy on failure.""" |
| dify_url = os.getenv("DIFY_API_URL", "http://127.0.0.1:8899") |
| dify_key = os.getenv("DIFY_APP_KEY", "") |
|
|
| if not dify_key: |
| return {"conversations": [], "error": "DIFY_APP_KEY not configured"} |
|
|
| try: |
| import httpx |
|
|
| client = _brightdata_client(timeout=10) if use_proxy else httpx.AsyncClient(timeout=10) |
| async with client as c: |
| resp = await c.get( |
| f"{dify_url}/v1/conversations", |
| headers={"Authorization": f"Bearer {dify_key}"}, |
| params={"limit": limit}, |
| ) |
| if resp.status_code == 200: |
| data = resp.json() |
| return { |
| "conversations": data.get("data", []), |
| "total": data.get("total", 0), |
| "proxy": use_proxy, |
| } |
| return { |
| "conversations": [], |
| "error": f"Dify HTTP {resp.status_code}", |
| "proxy": use_proxy, |
| } |
| except Exception as e: |
| if not use_proxy: |
| return await dify_conversations(request, _, limit, use_proxy=True) |
| return {"conversations": [], "error": str(e), "proxy": use_proxy} |
|
|
|
|
| @router.get("/dify/apps") |
| async def dify_apps(request: Request, _=Depends(_verify_admin), use_proxy: bool = False): |
| """Get Dify apps. Auto-fallback to Bright Data proxy.""" |
| dify_url = os.getenv("DIFY_API_URL", "http://127.0.0.1:8899") |
| dify_key = os.getenv("DIFY_APP_KEY", "") |
|
|
| if not dify_key: |
| return {"apps": [], "error": "DIFY_APP_KEY not configured"} |
|
|
| try: |
| import httpx |
|
|
| client = _brightdata_client(timeout=10) if use_proxy else httpx.AsyncClient(timeout=10) |
| async with client as c: |
| resp = await c.get(f"{dify_url}/v1/apps", headers={"Authorization": f"Bearer {dify_key}"}) |
| if resp.status_code == 200: |
| data = resp.json() |
| return { |
| "apps": data.get("data", []), |
| "total": data.get("total", 0), |
| "proxy": use_proxy, |
| } |
| return {"apps": [], "error": f"Dify HTTP {resp.status_code}", "proxy": use_proxy} |
| except Exception as e: |
| if not use_proxy: |
| return await dify_apps(request, _, use_proxy=True) |
| return {"apps": [], "error": str(e), "proxy": use_proxy} |
|
|
|
|
| @router.get("/dify/conversations/{conversation_id}/messages") |
| async def dify_messages(request: Request, conversation_id: str, _=Depends(_verify_admin), use_proxy: bool = False): |
| """Get messages for a Dify conversation. Auto-fallback to Bright Data proxy.""" |
| dify_url = os.getenv("DIFY_API_URL", "http://127.0.0.1:8899") |
| dify_key = os.getenv("DIFY_APP_KEY", "") |
|
|
| if not dify_key: |
| return {"messages": [], "error": "DIFY_APP_KEY not configured"} |
|
|
| try: |
| import httpx |
|
|
| client = _brightdata_client(timeout=10) if use_proxy else httpx.AsyncClient(timeout=10) |
| async with client as c: |
| resp = await c.get( |
| f"{dify_url}/v1/messages", |
| headers={"Authorization": f"Bearer {dify_key}"}, |
| params={"conversation_id": conversation_id, "limit": 100}, |
| ) |
| if resp.status_code == 200: |
| data = resp.json() |
| return { |
| "messages": data.get("data", []), |
| "total": data.get("total", 0), |
| "proxy": use_proxy, |
| } |
| return {"messages": [], "error": f"Dify HTTP {resp.status_code}", "proxy": use_proxy} |
| except Exception as e: |
| if not use_proxy: |
| return await dify_messages(request, conversation_id, _, use_proxy=True) |
| return {"messages": [], "error": str(e), "proxy": use_proxy} |
|
|
|
|
| class DifyChatRequest(BaseModel): |
| message: str |
| conversation_id: str | None = None |
| app_id: str | None = None |
|
|
|
|
| @router.post("/dify/chat") |
| async def dify_chat(request: Request, req: DifyChatRequest, _=Depends(_verify_admin)): |
| """Send a message to Dify advisor.""" |
| |
| dify_url = "http://172.23.0.4:5001" |
| dify_key = os.getenv("DIFY_APP_KEY", "app-nIpKWvytqS3JGNNmD0Iuz0cG") |
|
|
| if not dify_key: |
| return {"error": "DIFY_APP_KEY not configured"} |
|
|
| try: |
| import asyncio |
|
|
| loop = asyncio.get_event_loop() |
| payload = json.dumps( |
| { |
| "inputs": {}, |
| "query": req.message, |
| "response_mode": "blocking", |
| "conversation_id": req.conversation_id or "", |
| "user": "darkroom_admin", |
| } |
| ).encode() |
|
|
| |
| def _call_dify(): |
| import urllib.request as ur |
|
|
| http_req = ur.Request( |
| f"{dify_url}/v1/chat-messages", |
| data=payload, |
| headers={"Authorization": f"Bearer {dify_key}", "Content-Type": "application/json"}, |
| ) |
| resp = ur.urlopen(http_req, timeout=60) |
| return json.loads(resp.read()) |
|
|
| data = await loop.run_in_executor(None, _call_dify) |
| return { |
| "answer": data.get("answer", ""), |
| "conversation_id": data.get("conversation_id", ""), |
| "message_id": data.get("message_id", ""), |
| "status": "completed", |
| } |
| except Exception as e: |
| return {"error": str(e)[:300]} |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/processes") |
| async def admin_processes(request: Request, _=Depends(_verify_admin)): |
| """Get running processes.""" |
| processes = [] |
| try: |
| for proc in psutil.process_iter(["pid", "name", "cpu_percent", "memory_info", "num_threads", "status"]): |
| try: |
| info = proc.info |
| mem_mb = info.get("memory_info", type("", (), {"rss": 0})()) |
| processes.append( |
| { |
| "pid": info["pid"], |
| "name": info["name"], |
| "cpu_percent": info.get("cpu_percent", 0), |
| "memory_mb": round(mem_mb.rss / (1024 * 1024), 2) if hasattr(mem_mb, "rss") else 0, |
| "threads": info.get("num_threads", 0), |
| "status": info.get("status", "unknown"), |
| } |
| ) |
| except Exception: |
| pass |
| except Exception as e: |
| return {"error": str(e)} |
|
|
| |
| processes.sort(key=lambda x: x.get("memory_mb", 0), reverse=True) |
| return {"processes": processes[:50], "total": len(processes)} |
|
|
|
|
| @router.get("/disk") |
| async def admin_disk(request: Request, _=Depends(_verify_admin)): |
| """Get detailed disk usage.""" |
| disks = [] |
| try: |
| for part in psutil.disk_partitions(all=False): |
| try: |
| usage = psutil.disk_usage(part.mountpoint) |
| disks.append( |
| { |
| "device": part.device, |
| "mountpoint": part.mountpoint, |
| "fstype": part.fstype, |
| "total_gb": round(usage.total / (1024**3), 2), |
| "used_gb": round(usage.used / (1024**3), 2), |
| "free_gb": round(usage.free / (1024**3), 2), |
| "percent": round(usage.used / usage.total * 100, 1), |
| } |
| ) |
| except Exception: |
| pass |
| except Exception as e: |
| return {"error": str(e)} |
|
|
| return {"disks": disks} |
|
|
|
|
| @router.get("/network") |
| async def admin_network(request: Request, _=Depends(_verify_admin)): |
| """Get network interface stats.""" |
| interfaces = [] |
| try: |
| stats = psutil.net_io_counters(pernic=True) |
| for name, stat in stats.items(): |
| interfaces.append( |
| { |
| "name": name, |
| "bytes_sent_mb": round(stat.bytes_sent / (1024**2), 2), |
| "bytes_recv_mb": round(stat.bytes_recv / (1024**2), 2), |
| "packets_sent": stat.packets_sent, |
| "packets_recv": stat.packets_recv, |
| "errors_in": stat.errin, |
| "errors_out": stat.errout, |
| "drops_in": stat.dropin, |
| "drops_out": stat.dropout, |
| } |
| ) |
| except Exception as e: |
| return {"error": str(e)} |
|
|
| return {"interfaces": interfaces} |
|
|
|
|
| |
| |
| |
|
|
|
|
| @router.get("/mail/sent") |
| async def mail_sent(request: Request, _=Depends(_verify_admin), limit: int = Query(20, le=100)): |
| """Get sent emails (placeholder β requires SMTP sent tracking).""" |
| return {"emails": [], "total": 0, "note": "Sent folder requires SMTP sent tracking"} |
|
|
|
|
| @router.get("/mail/folders") |
| async def mail_folders(request: Request, _=Depends(_verify_admin)): |
| """Get email folders.""" |
| return { |
| "folders": [ |
| {"name": "INBOX", "count": 0, "unread": 0}, |
| {"name": "Sent", "count": 0, "unread": 0}, |
| {"name": "Drafts", "count": 0, "unread": 0}, |
| {"name": "Spam", "count": 0, "unread": 0}, |
| {"name": "Trash", "count": 0, "unread": 0}, |
| ] |
| } |
|
|
|
|
| @router.delete("/mail/{msg_id}") |
| async def mail_delete(request: Request, msg_id: str, _=Depends(_verify_admin)): |
| """Delete an email (placeholder).""" |
| return {"status": "deleted", "id": msg_id} |
|
|