Spaces:
Sleeping
Sleeping
| """ | |
| ============================================ | |
| π₯ RUHI-CORE v1.0.0 - COMPLETE PaaS Engine | |
| ============================================ | |
| Module A (Engine) + Module B (Files) + Module C (Dashboard) | |
| All combined into one powerful file. | |
| """ | |
| import os | |
| import json | |
| from contextlib import asynccontextmanager | |
| from pathlib import Path | |
| from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, Depends | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import HTMLResponse, JSONResponse, FileResponse | |
| from loguru import logger | |
| from core.config import settings | |
| from core.orchestrator import orchestrator | |
| from core.process_manager import process_manager | |
| from core.metrics_collector import metrics_collector | |
| from core.websocket_handler import ws_manager | |
| from core.reverse_proxy import reverse_proxy | |
| from core.auth import get_current_user, verify_token | |
| from api.routes import auth, services, metrics, terminal, files | |
| from api.middleware.ip_whitelist import IPWhitelistMiddleware | |
| async def lifespan(app: FastAPI): | |
| logger.info("π RUHI-CORE Engine starting...") | |
| await orchestrator.startup() | |
| yield | |
| logger.info("π RUHI-CORE Engine shutting down...") | |
| await orchestrator.shutdown() | |
| app = FastAPI( | |
| title="RUHI-CORE", | |
| description="Premium PaaS Platform", | |
| version="1.0.0", | |
| lifespan=lifespan, | |
| docs_url="/api/docs", | |
| redoc_url="/api/redoc" | |
| ) | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) | |
| app.add_middleware(IPWhitelistMiddleware) | |
| app.include_router(auth.router) | |
| app.include_router(services.router) | |
| app.include_router(metrics.router) | |
| app.include_router(terminal.router) | |
| app.include_router(files.router) | |
| # WebSocket endpoints | |
| async def dashboard_websocket(websocket: WebSocket): | |
| token = websocket.query_params.get("token", "") | |
| try: verify_token(token) | |
| except: await websocket.close(code=4001); return | |
| await ws_manager.connect_dashboard(websocket) | |
| try: | |
| while True: await websocket.receive_text() | |
| except WebSocketDisconnect: await ws_manager.disconnect_dashboard(websocket) | |
| except: await ws_manager.disconnect_dashboard(websocket) | |
| async def service_logs_websocket(websocket: WebSocket, service_id: str): | |
| token = websocket.query_params.get("token", "") | |
| try: verify_token(token) | |
| except: await websocket.close(code=4001); return | |
| service = process_manager.get_service(service_id) | |
| if not service: await websocket.close(code=4004); return | |
| await ws_manager.connect_logs(websocket, service_id) | |
| try: | |
| existing = list(service.log_buffer) | |
| await websocket.send_text(json.dumps({"type": "history", "logs": existing})) | |
| while True: await websocket.receive_text() | |
| except WebSocketDisconnect: await ws_manager.disconnect_logs(websocket, service_id) | |
| except: await ws_manager.disconnect_logs(websocket, service_id) | |
| async def proxy_to_service(service_name: str, path: str, request: Request): | |
| return await reverse_proxy.proxy_request(service_name, path, request) | |
| async def health_check(): | |
| return {"status": "healthy", "platform": "RUHI-CORE", "version": "1.0.0"} | |
| async def create_default_config(): | |
| config_file = settings.CONFIG_DIR / "ruhi.yaml" | |
| if not config_file.exists(): | |
| config_file.parent.mkdir(parents=True, exist_ok=True) | |
| config_file.write_text("platform: RUHI-CORE\nversion: 1.0.0\n") | |
| # ============================================================ | |
| # π₯π₯π₯ MEGA PREMIUM DASHBOARD - MODULE C π₯π₯π₯ | |
| # Cyberpunk Dark Mode | Glassmorphism | Animated | God-Level UI | |
| # ============================================================ | |
| async def serve_dashboard(): | |
| return HTMLResponse(content=MEGA_DASHBOARD_HTML) | |
| MEGA_DASHBOARD_HTML = '''<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RUHI-CORE | Premium PaaS Engine</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <link href="https://unpkg.com/lucide-static@latest/font/lucide.css" rel="stylesheet"> | |
| <style> | |
| :root{--neon:#00ffcc;--neon-rgb:0,255,204;--neon-dim:rgba(0,255,204,0.08);--neon-glow:rgba(0,255,204,0.25);--purple:#a855f7;--purple-dim:rgba(168,85,247,0.1);--bg-primary:#06060b;--bg-secondary:#0d0d14;--bg-tertiary:#13131e;--bg-card:rgba(16,16,28,0.85);--bg-glass:rgba(255,255,255,0.02);--bg-glass-hover:rgba(255,255,255,0.05);--text-primary:#eaeaed;--text-secondary:#71717a;--text-dim:#3f3f46;--border:rgba(255,255,255,0.05);--border-hover:rgba(0,255,204,0.25);--danger:#ff4757;--danger-dim:rgba(255,71,87,0.1);--warning:#ffa502;--warning-dim:rgba(255,165,2,0.1);--success:#2ed573;--success-dim:rgba(46,213,115,0.1);--info:#3b82f6;--info-dim:rgba(59,130,246,0.1);--radius:16px;--radius-sm:10px;--radius-xs:6px;--shadow:0 8px 32px rgba(0,0,0,0.4);--shadow-lg:0 20px 60px rgba(0,0,0,0.6);--transition:all 0.3s cubic-bezier(0.4,0,0.2,1)} | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| body{font-family:'Inter',system-ui,-apple-system,sans-serif;background:var(--bg-primary);color:var(--text-primary);min-height:100vh;overflow-x:hidden} | |
| /* Animated Background Grid */ | |
| body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellipse at 15% 50%,rgba(0,255,204,0.03) 0%,transparent 50%),radial-gradient(ellipse at 85% 15%,rgba(168,85,247,0.03) 0%,transparent 50%),radial-gradient(ellipse at 50% 85%,rgba(59,130,246,0.02) 0%,transparent 50%);z-index:0;pointer-events:none} | |
| body::after{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(255,255,255,0.01) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.01) 1px,transparent 1px);background-size:60px 60px;z-index:0;pointer-events:none;opacity:0.4} | |
| ::-webkit-scrollbar{width:5px;height:5px} | |
| ::-webkit-scrollbar-track{background:transparent} | |
| ::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:10px} | |
| ::-webkit-scrollbar-thumb:hover{background:rgba(0,255,204,0.2)} | |
| /* ========== ANIMATIONS ========== */ | |
| @keyframes fadeInUp{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}} | |
| @keyframes fadeInDown{from{opacity:0;transform:translateY(-24px)}to{opacity:1;transform:translateY(0)}} | |
| @keyframes fadeInLeft{from{opacity:0;transform:translateX(-24px)}to{opacity:1;transform:translateX(0)}} | |
| @keyframes fadeInRight{from{opacity:0;transform:translateX(24px)}to{opacity:1;transform:translateX(0)}} | |
| @keyframes fadeIn{from{opacity:0}to{opacity:1}} | |
| @keyframes slideUp{from{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}} | |
| @keyframes slideDown{from{transform:translateY(-100%)}to{transform:translateY(0)}} | |
| @keyframes scaleIn{from{transform:scale(0.9);opacity:0}to{transform:scale(1);opacity:1}} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| @keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}} | |
| @keyframes neonPulse{0%,100%{box-shadow:0 0 5px rgba(var(--neon-rgb),0.2),0 0 10px rgba(var(--neon-rgb),0.1)}50%{box-shadow:0 0 15px rgba(var(--neon-rgb),0.4),0 0 30px rgba(var(--neon-rgb),0.2)}} | |
| @keyframes borderGlow{0%,100%{border-color:rgba(var(--neon-rgb),0.1)}50%{border-color:rgba(var(--neon-rgb),0.4)}} | |
| @keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}} | |
| @keyframes typewriter{from{width:0}to{width:100%}} | |
| @keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}} | |
| @keyframes toastIn{from{transform:translateX(120%);opacity:0}to{transform:translateX(0);opacity:1}} | |
| @keyframes toastOut{from{transform:translateX(0);opacity:1}to{transform:translateX(120%);opacity:0}} | |
| @keyframes ripple{0%{transform:scale(0);opacity:0.5}100%{transform:scale(4);opacity:0}} | |
| .animate-in{animation:fadeInUp 0.5s ease-out both} | |
| .animate-in-1{animation:fadeInUp 0.5s ease-out 0.05s both} | |
| .animate-in-2{animation:fadeInUp 0.5s ease-out 0.1s both} | |
| .animate-in-3{animation:fadeInUp 0.5s ease-out 0.15s both} | |
| .animate-in-4{animation:fadeInUp 0.5s ease-out 0.2s both} | |
| .animate-in-5{animation:fadeInUp 0.5s ease-out 0.25s both} | |
| .animate-in-6{animation:fadeInUp 0.5s ease-out 0.3s both} | |
| .animate-in-7{animation:fadeInUp 0.5s ease-out 0.35s both} | |
| /* ========== LOGIN ========== */ | |
| .login-screen{display:flex;justify-content:center;align-items:center;min-height:100vh;position:relative;z-index:1;perspective:1000px} | |
| .login-card{background:var(--bg-card);backdrop-filter:blur(40px) saturate(1.5);border:1px solid var(--border);border-radius:28px;padding:52px 44px;width:460px;max-width:95vw;box-shadow:var(--shadow-lg),0 0 80px rgba(var(--neon-rgb),0.03);animation:fadeInUp 0.8s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden} | |
| .login-card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(var(--neon-rgb),0.3),transparent)} | |
| .login-card::after{content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:60%;height:1px;background:linear-gradient(90deg,transparent,rgba(168,85,247,0.2),transparent)} | |
| .login-header{text-align:center;margin-bottom:40px} | |
| .login-icon{width:64px;height:64px;border-radius:20px;background:linear-gradient(135deg,rgba(var(--neon-rgb),0.15),rgba(168,85,247,0.15));display:flex;align-items:center;justify-content:center;margin:0 auto 20px;font-size:28px;border:1px solid rgba(var(--neon-rgb),0.2);animation:float 3s ease-in-out infinite} | |
| .login-title{font-size:30px;font-weight:900;letter-spacing:-1.5px;background:linear-gradient(135deg,var(--neon),var(--purple),var(--info));background-size:200% 200%;animation:gradientShift 4s ease infinite;-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:6px} | |
| .login-subtitle{color:var(--text-secondary);font-size:14px;font-weight:400} | |
| .login-version{display:inline-flex;align-items:center;gap:4px;background:var(--neon-dim);color:var(--neon);padding:4px 12px;border-radius:20px;font-size:11px;font-weight:600;margin-top:10px;border:1px solid rgba(var(--neon-rgb),0.15)} | |
| .input-group{margin-bottom:22px;position:relative} | |
| .input-label{display:block;font-size:11px;font-weight:700;color:var(--text-secondary);margin-bottom:8px;text-transform:uppercase;letter-spacing:1px} | |
| .input-wrapper{position:relative} | |
| .input-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);color:var(--text-dim);font-size:16px;transition:color 0.3s;pointer-events:none} | |
| .input-field{width:100%;padding:15px 18px 15px 48px;background:var(--bg-glass);border:1.5px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-size:15px;font-family:'JetBrains Mono',monospace;transition:var(--transition);outline:none} | |
| .input-field:focus{border-color:var(--neon);box-shadow:0 0 0 3px var(--neon-dim),0 0 20px rgba(var(--neon-rgb),0.08)} | |
| .input-field:focus+.input-icon,.input-field:focus~.input-icon{color:var(--neon)} | |
| .input-field::placeholder{color:var(--text-dim);font-family:'Inter',sans-serif} | |
| .login-btn{width:100%;padding:17px;background:linear-gradient(135deg,var(--neon),#00cc99,#00b8d4);border:none;border-radius:var(--radius-sm);color:#000;font-size:15px;font-weight:800;cursor:pointer;transition:var(--transition);text-transform:uppercase;letter-spacing:1.5px;margin-top:8px;position:relative;overflow:hidden} | |
| .login-btn:hover{transform:translateY(-2px);box-shadow:0 8px 30px rgba(var(--neon-rgb),0.35)} | |
| .login-btn:active{transform:translateY(0)} | |
| .login-btn .btn-ripple{position:absolute;border-radius:50%;background:rgba(255,255,255,0.3);animation:ripple 0.6s linear} | |
| .login-error{background:var(--danger-dim);border:1px solid rgba(255,71,87,0.25);color:var(--danger);padding:13px 16px;border-radius:var(--radius-sm);font-size:13px;margin-bottom:18px;display:none;text-align:center;animation:fadeIn 0.3s} | |
| /* ========== LAYOUT ========== */ | |
| .app{display:none;min-height:100vh;position:relative;z-index:1} | |
| .app.active{display:flex} | |
| .sidebar{width:260px;background:var(--bg-secondary);border-right:1px solid var(--border);display:flex;flex-direction:column;position:fixed;top:0;bottom:0;left:0;z-index:50;transition:transform 0.3s;overflow-y:auto} | |
| .sidebar-header{padding:24px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px} | |
| .sidebar-logo{font-size:20px;font-weight:900;background:linear-gradient(135deg,var(--neon),var(--purple));-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.5px} | |
| .sidebar-version{font-size:10px;background:var(--neon-dim);color:var(--neon);padding:2px 8px;border-radius:10px;font-weight:700;border:1px solid rgba(var(--neon-rgb),0.15)} | |
| .sidebar-nav{flex:1;padding:16px 12px;display:flex;flex-direction:column;gap:2px} | |
| .nav-section{font-size:10px;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:1.5px;padding:16px 12px 8px;margin-top:4px} | |
| .nav-item{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:var(--radius-sm);color:var(--text-secondary);cursor:pointer;font-size:13.5px;font-weight:500;transition:var(--transition);position:relative;border:1px solid transparent} | |
| .nav-item:hover{background:var(--bg-glass-hover);color:var(--text-primary);border-color:var(--border)} | |
| .nav-item.active{background:var(--neon-dim);color:var(--neon);border-color:rgba(var(--neon-rgb),0.15);font-weight:600} | |
| .nav-item.active::before{content:'';position:absolute;left:-12px;top:50%;transform:translateY(-50%);width:3px;height:24px;background:var(--neon);border-radius:0 4px 4px 0} | |
| .nav-item .nav-icon{font-size:18px;width:20px;text-align:center} | |
| .nav-item .nav-badge{margin-left:auto;background:var(--neon);color:#000;padding:1px 8px;border-radius:10px;font-size:10px;font-weight:800} | |
| .sidebar-footer{padding:16px 12px;border-top:1px solid var(--border)} | |
| .sidebar-footer .status-bar{display:flex;align-items:center;gap:8px;padding:10px 14px;border-radius:var(--radius-sm);background:var(--bg-glass);font-size:12px;color:var(--text-secondary)} | |
| .status-dot{width:7px;height:7px;border-radius:50%;background:var(--success);animation:pulse 2s infinite;flex-shrink:0} | |
| .status-dot.offline{background:var(--danger)} | |
| .status-dot.warning{background:var(--warning)} | |
| .main-area{margin-left:260px;flex:1;display:flex;flex-direction:column;min-height:100vh} | |
| /* ========== TOP BAR ========== */ | |
| .topbar{background:rgba(6,6,11,0.85);backdrop-filter:blur(20px) saturate(1.2);border-bottom:1px solid var(--border);padding:0 28px;height:60px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:40} | |
| .topbar-left{display:flex;align-items:center;gap:16px} | |
| .topbar-breadcrumb{display:flex;align-items:center;gap:6px;font-size:13px} | |
| .topbar-breadcrumb span{color:var(--text-dim)} | |
| .topbar-breadcrumb .current{color:var(--text-primary);font-weight:600} | |
| .topbar-right{display:flex;align-items:center;gap:12px} | |
| .topbar-clock{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--text-dim);padding:6px 12px;background:var(--bg-glass);border-radius:var(--radius-xs);border:1px solid var(--border)} | |
| .topbar-ws{display:flex;align-items:center;gap:6px;font-size:12px;padding:6px 12px;background:var(--bg-glass);border-radius:var(--radius-xs);border:1px solid var(--border)} | |
| .icon-btn{width:36px;height:36px;border-radius:var(--radius-xs);border:1px solid var(--border);background:var(--bg-glass);color:var(--text-secondary);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:var(--transition);font-size:16px} | |
| .icon-btn:hover{border-color:var(--neon);color:var(--neon);background:var(--neon-dim)} | |
| /* ========== CONTENT ========== */ | |
| .page-content{padding:28px;flex:1} | |
| .page-header{margin-bottom:28px} | |
| .page-title{font-size:24px;font-weight:800;letter-spacing:-0.5px;margin-bottom:4px} | |
| .page-desc{font-size:14px;color:var(--text-secondary)} | |
| /* ========== GLASS CARD ========== */ | |
| .glass-card{background:var(--bg-card);backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius);padding:24px;transition:var(--transition);position:relative;overflow:hidden} | |
| .glass-card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.05),transparent);opacity:0.8} | |
| .glass-card:hover{border-color:var(--border-hover);box-shadow:0 4px 20px rgba(0,0,0,0.2)} | |
| /* ========== STAT CARDS ========== */ | |
| .stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:16px;margin-bottom:28px} | |
| .stat-card{background:var(--bg-card);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:22px;transition:var(--transition);position:relative;overflow:hidden;cursor:default} | |
| .stat-card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(var(--neon-rgb),0.2),transparent);opacity:0;transition:opacity 0.3s} | |
| .stat-card::after{content:'';position:absolute;inset:0;background:radial-gradient(circle at 90% 10%,rgba(var(--neon-rgb),0.03),transparent 60%);opacity:0;transition:opacity 0.3s} | |
| .stat-card:hover{border-color:var(--border-hover);transform:translateY(-3px);box-shadow:0 12px 40px rgba(0,0,0,0.2)} | |
| .stat-card:hover::before,.stat-card:hover::after{opacity:1} | |
| .stat-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:14px} | |
| .stat-icon{width:42px;height:42px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0} | |
| .stat-icon.cpu{background:var(--neon-dim);color:var(--neon)} | |
| .stat-icon.mem{background:var(--purple-dim);color:var(--purple)} | |
| .stat-icon.disk{background:var(--warning-dim);color:var(--warning)} | |
| .stat-icon.svc{background:var(--success-dim);color:var(--success)} | |
| .stat-icon.net{background:var(--info-dim);color:var(--info)} | |
| .stat-icon.time{background:rgba(236,72,153,0.1);color:#ec4899} | |
| .stat-icon.proc{background:rgba(245,158,11,0.1);color:#f59e0b} | |
| .stat-icon.ws{background:rgba(99,102,241,0.1);color:#6366f1} | |
| .stat-trend{font-size:11px;font-weight:600;padding:2px 8px;border-radius:8px;display:flex;align-items:center;gap:3px} | |
| .stat-trend.up{color:var(--success);background:var(--success-dim)} | |
| .stat-trend.down{color:var(--danger);background:var(--danger-dim)} | |
| .stat-trend.neutral{color:var(--text-secondary);background:var(--bg-glass)} | |
| .stat-value{font-size:30px;font-weight:900;font-family:'JetBrains Mono',monospace;letter-spacing:-1px;line-height:1;margin-bottom:6px} | |
| .stat-value .stat-unit{font-size:14px;font-weight:500;color:var(--text-secondary);margin-left:2px} | |
| .stat-label{font-size:12px;color:var(--text-secondary);font-weight:500;margin-bottom:10px} | |
| .progress-track{width:100%;height:5px;background:var(--bg-glass);border-radius:10px;overflow:hidden} | |
| .progress-bar{height:100%;border-radius:10px;transition:width 0.8s cubic-bezier(0.4,0,0.2,1);position:relative} | |
| .progress-bar::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.2),transparent);animation:shimmer 2s infinite} | |
| .progress-bar.neon{background:linear-gradient(90deg,var(--neon),#00cc99)} | |
| .progress-bar.purple{background:linear-gradient(90deg,var(--purple),#c084fc)} | |
| .progress-bar.warning{background:linear-gradient(90deg,var(--warning),#fb923c)} | |
| .progress-bar.danger{background:linear-gradient(90deg,var(--danger),#ff6b6b)} | |
| .progress-bar.info{background:linear-gradient(90deg,var(--info),#60a5fa)} | |
| .stat-sub{font-size:11px;color:var(--text-dim);margin-top:8px;font-family:'JetBrains Mono',monospace} | |
| /* ========== SERVICE CARDS ========== */ | |
| .services-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(400px,1fr));gap:16px;margin-bottom:28px} | |
| .svc-card{background:var(--bg-card);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:24px;transition:var(--transition);position:relative;overflow:hidden} | |
| .svc-card:hover{border-color:var(--border-hover)} | |
| .svc-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;opacity:0;transition:opacity 0.3s} | |
| .svc-card.running::before{background:linear-gradient(90deg,transparent,var(--success),transparent);opacity:1} | |
| .svc-card.crashed::before{background:linear-gradient(90deg,transparent,var(--danger),transparent);opacity:1;animation:pulse 2s infinite} | |
| .svc-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px} | |
| .svc-name{font-size:17px;font-weight:700;display:flex;align-items:center;gap:10px} | |
| .svc-name .svc-type-icon{font-size:20px} | |
| .svc-badge{padding:5px 14px;border-radius:20px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;display:flex;align-items:center;gap:5px} | |
| .svc-badge.running{background:var(--success-dim);color:var(--success);border:1px solid rgba(46,213,115,0.25)} | |
| .svc-badge.running::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--success);animation:pulse 1.5s infinite} | |
| .svc-badge.stopped{background:var(--bg-glass);color:var(--text-secondary);border:1px solid var(--border)} | |
| .svc-badge.crashed{background:var(--danger-dim);color:var(--danger);border:1px solid rgba(255,71,87,0.25);animation:neonPulse 2s infinite} | |
| .svc-badge.starting,.svc-badge.restarting{background:var(--warning-dim);color:var(--warning);border:1px solid rgba(255,165,2,0.25)} | |
| .svc-info{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:18px} | |
| .svc-info-item{display:flex;flex-direction:column;gap:2px} | |
| .svc-info-label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;font-weight:600} | |
| .svc-info-value{font-size:13px;color:var(--text-primary);font-weight:500;font-family:'JetBrains Mono',monospace} | |
| .svc-actions{display:flex;gap:6px;flex-wrap:wrap} | |
| /* ========== BUTTONS ========== */ | |
| .btn{padding:8px 16px;border-radius:var(--radius-xs);border:1px solid var(--border);background:var(--bg-glass);color:var(--text-primary);cursor:pointer;font-size:12px;font-weight:600;transition:var(--transition);display:inline-flex;align-items:center;gap:6px;font-family:'Inter',sans-serif;position:relative;overflow:hidden;white-space:nowrap} | |
| .btn:hover{border-color:rgba(var(--neon-rgb),0.3);color:var(--neon);background:var(--neon-dim)} | |
| .btn:active{transform:scale(0.97)} | |
| .btn-sm{padding:5px 10px;font-size:11px;border-radius:var(--radius-xs)} | |
| .btn-lg{padding:12px 28px;font-size:14px} | |
| .btn-primary{background:linear-gradient(135deg,var(--neon),#00cc99);border:none;color:#000;font-weight:800} | |
| .btn-primary:hover{box-shadow:0 4px 20px rgba(var(--neon-rgb),0.35);transform:translateY(-1px);color:#000} | |
| .btn-success{border-color:rgba(46,213,115,0.3);color:var(--success)} | |
| .btn-success:hover{background:var(--success-dim);border-color:rgba(46,213,115,0.5);color:var(--success)} | |
| .btn-danger{border-color:rgba(255,71,87,0.3);color:var(--danger)} | |
| .btn-danger:hover{background:var(--danger-dim);border-color:rgba(255,71,87,0.5);color:var(--danger)} | |
| .btn-warning{border-color:rgba(255,165,2,0.3);color:var(--warning)} | |
| .btn-warning:hover{background:var(--warning-dim);border-color:rgba(255,165,2,0.5);color:var(--warning)} | |
| .btn-ghost{border:none;background:transparent;color:var(--text-secondary)} | |
| .btn-ghost:hover{color:var(--text-primary);background:var(--bg-glass-hover)} | |
| /* ========== FORMS ========== */ | |
| .form-panel{background:var(--bg-card);backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:28px;margin-bottom:24px;position:relative;overflow:hidden} | |
| .form-panel::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(var(--neon-rgb),0.15),transparent)} | |
| .form-panel h3{font-size:17px;font-weight:800;margin-bottom:20px;display:flex;align-items:center;gap:10px;color:var(--neon)} | |
| .form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:16px} | |
| .form-field label{display:block;font-size:10px;font-weight:700;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.8px} | |
| .form-field input,.form-field select,.form-field textarea{width:100%;padding:11px 14px;background:var(--bg-glass);border:1.5px solid var(--border);border-radius:var(--radius-xs);color:var(--text-primary);font-size:13px;outline:none;transition:var(--transition);font-family:'Inter',sans-serif} | |
| .form-field input:focus,.form-field select:focus,.form-field textarea:focus{border-color:var(--neon);box-shadow:0 0 0 3px var(--neon-dim)} | |
| .form-field select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:36px} | |
| .form-field select option{background:var(--bg-secondary);color:var(--text-primary)} | |
| .form-field textarea{font-family:'JetBrains Mono',monospace;resize:vertical;min-height:80px} | |
| /* ========== LOGS ========== */ | |
| .logs-container{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} | |
| .logs-header{padding:14px 20px;background:rgba(255,255,255,0.02);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center} | |
| .logs-header .logs-title{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px} | |
| .logs-body{padding:14px 18px;max-height:450px;overflow-y:auto;font-family:'JetBrains Mono',monospace;font-size:12px;line-height:1.9;color:var(--text-secondary)} | |
| .log-line{padding:1px 4px;border-radius:3px;transition:background 0.15s} | |
| .log-line:hover{background:rgba(255,255,255,0.02)} | |
| /* ========== TERMINAL ========== */ | |
| .terminal-container{background:#060609;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;position:relative} | |
| .terminal-titlebar{padding:11px 18px;background:rgba(255,255,255,0.025);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px} | |
| .terminal-dot{width:11px;height:11px;border-radius:50%} | |
| .terminal-body{padding:16px 18px;min-height:350px;max-height:550px;overflow-y:auto;font-family:'JetBrains Mono',monospace;font-size:12.5px;line-height:1.7} | |
| .terminal-output{color:var(--text-secondary);white-space:pre-wrap;word-break:break-all} | |
| .terminal-cmd{color:var(--neon)} | |
| .terminal-err{color:var(--danger)} | |
| .terminal-input-row{display:flex;align-items:center;gap:8px;padding:10px 18px;border-top:1px solid var(--border);background:rgba(255,255,255,0.015)} | |
| .terminal-prompt{color:var(--neon);font-family:'JetBrains Mono',monospace;font-weight:800;font-size:13px;flex-shrink:0} | |
| .terminal-input{flex:1;background:transparent;border:none;color:var(--text-primary);font-family:'JetBrains Mono',monospace;font-size:13px;outline:none} | |
| /* ========== FILE MANAGER ========== */ | |
| .fm-toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:16px} | |
| .fm-toolbar .fm-search{padding:9px 14px 9px 36px;background:var(--bg-glass);border:1.5px solid var(--border);border-radius:var(--radius-xs);color:var(--text-primary);font-size:13px;outline:none;min-width:240px;transition:var(--transition)} | |
| .fm-toolbar .fm-search:focus{border-color:var(--neon);box-shadow:0 0 0 3px var(--neon-dim)} | |
| .fm-breadcrumb{display:flex;align-items:center;gap:4px;font-size:13px;margin-bottom:14px;flex-wrap:wrap;padding:10px 16px;background:var(--bg-glass);border:1px solid var(--border);border-radius:var(--radius-sm)} | |
| .fm-breadcrumb a{color:var(--neon);text-decoration:none;cursor:pointer;padding:3px 8px;border-radius:var(--radius-xs);transition:var(--transition)} | |
| .fm-breadcrumb a:hover{background:var(--neon-dim)} | |
| .fm-breadcrumb .sep{color:var(--text-dim);font-size:10px} | |
| .fm-breadcrumb .current-dir{color:var(--text-primary);font-weight:600;padding:3px 8px} | |
| .fm-table-wrapper{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden} | |
| .fm-table{width:100%;border-collapse:collapse} | |
| .fm-table th{text-align:left;padding:13px 18px;font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid var(--border);font-weight:700;background:rgba(255,255,255,0.015)} | |
| .fm-table td{padding:11px 18px;border-bottom:1px solid rgba(255,255,255,0.025);font-size:13px;transition:background 0.15s} | |
| .fm-table tr:hover td{background:var(--bg-glass-hover)} | |
| .fm-table tr:last-child td{border-bottom:none} | |
| .fm-name{cursor:pointer;display:flex;align-items:center;gap:10px;transition:color 0.2s} | |
| .fm-name:hover{color:var(--neon)} | |
| .fm-icon{font-size:18px;flex-shrink:0} | |
| .fm-size{color:var(--text-dim);font-family:'JetBrains Mono',monospace;font-size:11.5px} | |
| .fm-date{color:var(--text-dim);font-size:11.5px} | |
| .fm-actions{display:flex;gap:4px} | |
| .fm-action{padding:4px 7px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--text-dim);cursor:pointer;font-size:13px;transition:var(--transition)} | |
| .fm-action:hover{border-color:var(--neon);color:var(--neon);background:var(--neon-dim)} | |
| .fm-action.del:hover{border-color:var(--danger);color:var(--danger);background:var(--danger-dim)} | |
| /* ========== CODE EDITOR ========== */ | |
| .editor-panel{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;margin-top:20px;display:none} | |
| .editor-panel.active{display:block;animation:fadeInUp 0.3s ease} | |
| .editor-header{padding:12px 20px;background:rgba(255,255,255,0.02);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center} | |
| .editor-title{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:700} | |
| .editor-meta{font-size:11px;color:var(--text-dim);font-family:'JetBrains Mono',monospace} | |
| .editor-textarea{width:100%;min-height:520px;padding:20px;background:#080810;border:none;color:var(--text-primary);font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.75;resize:vertical;outline:none;tab-size:4;-moz-tab-size:4} | |
| .editor-textarea:focus{box-shadow:inset 0 0 0 1px rgba(var(--neon-rgb),0.1)} | |
| /* ========== DROP ZONE ========== */ | |
| .drop-zone{border:2px dashed rgba(var(--neon-rgb),0.15);border-radius:var(--radius);padding:56px 32px;text-align:center;transition:var(--transition);cursor:pointer;position:relative;background:var(--bg-glass)} | |
| .drop-zone:hover,.drop-zone.active{border-color:var(--neon);background:var(--neon-dim);box-shadow:inset 0 0 30px rgba(var(--neon-rgb),0.03)} | |
| .drop-zone h3{font-size:20px;margin-bottom:8px;color:var(--text-primary)} | |
| .drop-zone p{color:var(--text-secondary);font-size:14px;margin-bottom:16px} | |
| .drop-zone input[type="file"]{position:absolute;inset:0;opacity:0;cursor:pointer} | |
| .drop-zone .drop-formats{font-size:11px;color:var(--text-dim)} | |
| .drop-zone .drop-icon{font-size:48px;margin-bottom:16px;display:block;animation:float 3s ease-in-out infinite} | |
| /* Deploy Steps */ | |
| .deploy-steps{margin-top:20px;display:none} | |
| .deploy-step{display:flex;align-items:center;gap:14px;padding:10px 0;font-size:13px;position:relative} | |
| .deploy-step::before{content:'';position:absolute;left:14px;top:34px;width:1px;height:calc(100% - 14px);background:var(--border)} | |
| .deploy-step:last-child::before{display:none} | |
| .step-bullet{width:28px;height:28px;border-radius:50%;border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0;background:var(--bg-secondary);z-index:1} | |
| .deploy-step.done .step-bullet{border-color:var(--success);background:var(--success-dim);color:var(--success)} | |
| .deploy-step.active .step-bullet{border-color:var(--neon);background:var(--neon-dim);color:var(--neon);animation:neonPulse 1.5s infinite} | |
| .deploy-step.error .step-bullet{border-color:var(--danger);background:var(--danger-dim);color:var(--danger)} | |
| .step-text{color:var(--text-dim)} | |
| .deploy-step.done .step-text{color:var(--success)} | |
| .deploy-step.active .step-text{color:var(--neon);font-weight:600} | |
| .deploy-step.error .step-text{color:var(--danger)} | |
| .deploy-result{margin-top:20px;display:none} | |
| /* ========== TOAST ========== */ | |
| .toast-stack{position:fixed;top:72px;right:20px;z-index:99999;display:flex;flex-direction:column;gap:8px} | |
| .toast-item{padding:14px 20px;border-radius:var(--radius-sm);font-size:13px;font-weight:500;animation:toastIn 0.4s cubic-bezier(0.16,1,0.3,1);backdrop-filter:blur(16px);min-width:300px;border:1px solid;display:flex;align-items:center;gap:10px;box-shadow:var(--shadow)} | |
| .toast-item.removing{animation:toastOut 0.3s ease forwards} | |
| .toast-item.success{background:rgba(46,213,115,0.12);border-color:rgba(46,213,115,0.2);color:var(--success)} | |
| .toast-item.error{background:rgba(255,71,87,0.12);border-color:rgba(255,71,87,0.2);color:var(--danger)} | |
| .toast-item.info{background:rgba(var(--neon-rgb),0.08);border-color:rgba(var(--neon-rgb),0.2);color:var(--neon)} | |
| .toast-item.warning{background:rgba(255,165,2,0.12);border-color:rgba(255,165,2,0.2);color:var(--warning)} | |
| .toast-icon{font-size:18px;flex-shrink:0} | |
| /* ========== MODAL ========== */ | |
| .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.75);backdrop-filter:blur(6px);z-index:9999;display:none;justify-content:center;align-items:center} | |
| .modal-overlay.open{display:flex} | |
| .modal-box{background:var(--bg-card);backdrop-filter:blur(30px);border:1px solid var(--border);border-radius:20px;padding:32px;max-width:640px;width:95vw;max-height:85vh;overflow-y:auto;animation:scaleIn 0.3s cubic-bezier(0.16,1,0.3,1)} | |
| .modal-box h3{font-size:18px;font-weight:800;margin-bottom:20px;color:var(--neon);display:flex;align-items:center;gap:10px} | |
| .modal-footer{display:flex;gap:8px;justify-content:flex-end;margin-top:24px;padding-top:16px;border-top:1px solid var(--border)} | |
| /* ========== METRIC CHARTS (CSS only) ========== */ | |
| .mini-chart{display:flex;align-items:flex-end;gap:2px;height:40px;margin-top:8px} | |
| .mini-bar{width:4px;border-radius:2px;background:linear-gradient(to top,var(--neon),rgba(var(--neon-rgb),0.3));transition:height 0.3s} | |
| /* ========== RESPONSIVE ========== */ | |
| @media(max-width:1024px){ | |
| .sidebar{transform:translateX(-100%)} | |
| .sidebar.open{transform:translateX(0)} | |
| .main-area{margin-left:0} | |
| .stats-grid{grid-template-columns:repeat(2,1fr)} | |
| .services-grid{grid-template-columns:1fr} | |
| } | |
| @media(max-width:640px){ | |
| .stats-grid{grid-template-columns:1fr} | |
| .page-content{padding:16px} | |
| .form-grid{grid-template-columns:1fr} | |
| .topbar{padding:0 14px} | |
| } | |
| /* Page transitions */ | |
| .page{display:none} | |
| .page.active{display:block;animation:fadeInUp 0.35s ease-out both} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="toast-stack" id="toastStack"></div> | |
| <div class="modal-overlay" id="modalOverlay" onclick="if(event.target===this)closeModal()"><div class="modal-box" id="modalBox"></div></div> | |
| <!-- ====== LOGIN ====== --> | |
| <div class="login-screen" id="loginScreen"> | |
| <div class="login-card"> | |
| <div class="login-header"> | |
| <div class="login-icon">β‘</div> | |
| <div class="login-title">RUHI-CORE</div> | |
| <div class="login-subtitle">Premium PaaS Engine</div> | |
| <div class="login-version">π v1.0.0 β’ Enterprise</div> | |
| </div> | |
| <div class="login-error" id="loginError"></div> | |
| <div class="input-group"> | |
| <div class="input-label">Username</div> | |
| <div class="input-wrapper"> | |
| <input class="input-field" type="text" id="loginUser" placeholder="Admin username" autocomplete="off"> | |
| <span class="input-icon">π€</span> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <div class="input-label">Password</div> | |
| <div class="input-wrapper"> | |
| <input class="input-field" type="password" id="loginPass" placeholder="Admin password" autocomplete="off"> | |
| <span class="input-icon">π</span> | |
| </div> | |
| </div> | |
| <button class="login-btn" onclick="doLogin()">β‘ ACCESS DASHBOARD</button> | |
| </div> | |
| </div> | |
| <!-- ====== APP ====== --> | |
| <div class="app" id="appContainer"> | |
| <!-- Sidebar --> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <span style="font-size:24px">β‘</span> | |
| <span class="sidebar-logo">RUHI-CORE</span> | |
| <span class="sidebar-version">v1.0</span> | |
| </div> | |
| <nav class="sidebar-nav"> | |
| <div class="nav-section">Main</div> | |
| <div class="nav-item active" onclick="go('dashboard',this)" data-page="dashboard"><span class="nav-icon">π</span>Dashboard</div> | |
| <div class="nav-item" onclick="go('services',this)" data-page="services"><span class="nav-icon">π§</span>Services<span class="nav-badge" id="svcCount">0</span></div> | |
| <div class="nav-section">Tools</div> | |
| <div class="nav-item" onclick="go('files',this)" data-page="files"><span class="nav-icon">π</span>File Manager</div> | |
| <div class="nav-item" onclick="go('deploy',this)" data-page="deploy"><span class="nav-icon">π</span>Deploy</div> | |
| <div class="nav-item" onclick="go('editor',this)" data-page="editor"><span class="nav-icon">βοΈ</span>Code Editor</div> | |
| <div class="nav-section">Monitor</div> | |
| <div class="nav-item" onclick="go('logs',this)" data-page="logs"><span class="nav-icon">π</span>Logs</div> | |
| <div class="nav-item" onclick="go('terminal',this)" data-page="terminal"><span class="nav-icon">π₯οΈ</span>Terminal</div> | |
| <div class="nav-item" onclick="go('metrics',this)" data-page="metrics"><span class="nav-icon">π</span>Metrics</div> | |
| <div class="nav-section">System</div> | |
| <div class="nav-item" onclick="go('settings',this)" data-page="settings"><span class="nav-icon">βοΈ</span>Settings</div> | |
| </nav> | |
| <div class="sidebar-footer"> | |
| <div class="status-bar"><div class="status-dot" id="statusDot"></div><span id="statusText">Engine Online</span></div> | |
| </div> | |
| </div> | |
| <!-- Main Area --> | |
| <div class="main-area"> | |
| <!-- Top Bar --> | |
| <div class="topbar"> | |
| <div class="topbar-left"> | |
| <button class="icon-btn" onclick="toggleSidebar()" style="display:none" id="menuBtn">β°</button> | |
| <div class="topbar-breadcrumb"><span>RUHI-CORE</span><span>/</span><span class="current" id="pageLabel">Dashboard</span></div> | |
| </div> | |
| <div class="topbar-right"> | |
| <div class="topbar-ws"><div class="status-dot" id="wsDot"></div><span id="wsLabel">Connecting</span></div> | |
| <div class="topbar-clock" id="clock">00:00:00</div> | |
| <button class="icon-btn" onclick="refreshAll()" title="Refresh">π</button> | |
| <button class="icon-btn" onclick="doLogout()" title="Logout" style="color:var(--danger)">β</button> | |
| </div> | |
| </div> | |
| <div class="page-content"> | |
| <!-- ====== DASHBOARD PAGE ====== --> | |
| <div class="page active" id="page-dashboard"> | |
| <div class="page-header animate-in"><div class="page-title">Dashboard Overview</div><div class="page-desc">Real-time system monitoring and service status</div></div> | |
| <div class="stats-grid" id="statsGrid"></div> | |
| <div class="page-header" style="margin-top:8px"><div class="page-title" style="font-size:18px">Active Services</div></div> | |
| <div class="services-grid" id="dashSvcGrid"></div> | |
| </div> | |
| <!-- ====== SERVICES PAGE ====== --> | |
| <div class="page" id="page-services"> | |
| <div class="page-header"><div class="page-title">Services</div><div class="page-desc">Create, manage and monitor your services</div></div> | |
| <div class="form-panel"> | |
| <h3>π Create New Service</h3> | |
| <div class="form-grid"> | |
| <div class="form-field"><label>Service Name</label><input type="text" id="svcName" placeholder="my-bot"></div> | |
| <div class="form-field"><label>Type</label><select id="svcType"><option value="web">π Web Service</option><option value="worker">βοΈ Worker</option><option value="bot">π€ Bot</option></select></div> | |
| <div class="form-field"><label>Language</label><select id="svcLang"><option value="python">π Python</option><option value="node">π’ Node.js</option><option value="shell">π Shell</option></select></div> | |
| <div class="form-field"><label>Entry File</label><input type="text" id="svcEntry" placeholder="main.py"></div> | |
| </div> | |
| <div class="form-grid"> | |
| <div class="form-field"><label>Command (optional)</label><input type="text" id="svcCmd" placeholder="python main.py"></div> | |
| <div class="form-field"><label>Description</label><input type="text" id="svcDesc" placeholder="My service"></div> | |
| </div> | |
| <button class="btn btn-primary btn-lg" onclick="createSvc()" style="margin-top:4px">β‘ Create & Deploy</button> | |
| </div> | |
| <div class="services-grid" id="svcGrid"></div> | |
| </div> | |
| <!-- ====== FILES PAGE ====== --> | |
| <div class="page" id="page-files"> | |
| <div class="page-header"><div class="page-title">File Manager</div><div class="page-desc">Browse, edit and manage your files</div></div> | |
| <div class="fm-toolbar"> | |
| <input class="fm-search" id="fmSearch" placeholder="π Search files..." onkeydown="if(event.key==='Enter')searchFM()"> | |
| <button class="btn" onclick="browseFM(curPath)">π Refresh</button> | |
| <button class="btn" onclick="newFileFM()">π New File</button> | |
| <button class="btn" onclick="newDirFM()">π New Folder</button> | |
| <button class="btn" onclick="uploadFM()">π€ Upload</button> | |
| <button class="btn" onclick="diskUsageFM()">πΎ Disk</button> | |
| </div> | |
| <div class="fm-breadcrumb" id="fmBread"></div> | |
| <div class="fm-table-wrapper"> | |
| <table class="fm-table"><thead><tr><th style="width:42%">Name</th><th style="width:14%">Size</th><th style="width:18%">Modified</th><th style="width:8%">Perms</th><th style="width:18%">Actions</th></tr></thead><tbody id="fmBody"><tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-dim)">Loading...</td></tr></tbody></table> | |
| </div> | |
| <div id="fmInfo" style="font-size:11px;color:var(--text-dim);margin-top:8px;padding:0 4px"></div> | |
| </div> | |
| <!-- ====== EDITOR PAGE ====== --> | |
| <div class="page" id="page-editor"> | |
| <div class="page-header"><div class="page-title">Code Editor</div><div class="page-desc">Edit files with syntax highlighting</div></div> | |
| <div class="editor-panel active" id="editorBox"> | |
| <div class="editor-header"> | |
| <div class="editor-title"><span id="edIcon">π</span><span id="edName">No file open</span><span class="sidebar-version" id="edLang">-</span></div> | |
| <div style="display:flex;gap:8px;align-items:center"><span class="editor-meta" id="edMeta">Open a file to edit</span><button class="btn btn-primary" onclick="saveED()">πΎ Save</button><button class="btn" onclick="closeED()">β Close</button></div> | |
| </div> | |
| <textarea class="editor-textarea" id="edArea" spellcheck="false" placeholder="Open a file from the File Manager to start editing..."></textarea> | |
| </div> | |
| </div> | |
| <!-- ====== DEPLOY PAGE ====== --> | |
| <div class="page" id="page-deploy"> | |
| <div class="page-header"><div class="page-title">Quick Deploy</div><div class="page-desc">Upload a ZIP and launch instantly</div></div> | |
| <div class="form-panel"> | |
| <h3>π¦ Drag & Drop Deploy</h3> | |
| <div class="drop-zone" id="dropZone"> | |
| <span class="drop-icon">π¦</span> | |
| <h3>Drop your ZIP here</h3> | |
| <p>or click to browse</p> | |
| <input type="file" id="deployFile" accept=".zip,.tar,.tar.gz,.tgz,.7z,.rar" onchange="handleDeploy(this.files[0])"> | |
| <div class="drop-formats">Supports: .zip .tar.gz .tar .7z .rar β’ Max 500MB</div> | |
| </div> | |
| <div class="form-grid" style="margin-top:16px"> | |
| <div class="form-field"><label>Service Name (optional)</label><input type="text" id="deployName" placeholder="auto-detected"></div> | |
| <div class="form-field" style="display:flex;align-items:flex-end"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding-bottom:12px"><input type="checkbox" id="deployAuto" checked style="width:auto"> Auto-start</label></div> | |
| </div> | |
| <div class="deploy-steps" id="deploySteps"> | |
| <div class="deploy-step" id="ds-upload"><div class="step-bullet">1</div><span class="step-text">Uploading archive...</span></div> | |
| <div class="deploy-step" id="ds-extract"><div class="step-bullet">2</div><span class="step-text">Extracting files...</span></div> | |
| <div class="deploy-step" id="ds-detect"><div class="step-bullet">3</div><span class="step-text">Detecting project type...</span></div> | |
| <div class="deploy-step" id="ds-deps"><div class="step-bullet">4</div><span class="step-text">Installing dependencies...</span></div> | |
| <div class="deploy-step" id="ds-launch"><div class="step-bullet">5</div><span class="step-text">Launching service...</span></div> | |
| </div> | |
| <div class="deploy-result" id="deployResult"></div> | |
| </div> | |
| </div> | |
| <!-- ====== LOGS PAGE ====== --> | |
| <div class="page" id="page-logs"> | |
| <div class="page-header"><div class="page-title">Live Logs</div><div class="page-desc">Real-time service log viewer</div></div> | |
| <div style="margin-bottom:16px"><select id="logSelect" onchange="loadLogs()" style="padding:10px 16px;background:var(--bg-glass);border:1.5px solid var(--border);border-radius:var(--radius-xs);color:var(--text-primary);font-size:14px;min-width:220px;outline:none"><option value="">Select a service...</option></select></div> | |
| <div class="logs-container"><div class="logs-header"><span class="logs-title" id="logTitle">π Logs</span><button class="btn btn-sm" onclick="loadLogs()">π</button></div><div class="logs-body" id="logBody"><div style="text-align:center;padding:40px;color:var(--text-dim)">Select a service above</div></div></div> | |
| </div> | |
| <!-- ====== TERMINAL PAGE ====== --> | |
| <div class="page" id="page-terminal"> | |
| <div class="page-header"><div class="page-title">Terminal</div><div class="page-desc">Execute commands directly</div></div> | |
| <div class="terminal-container"><div class="terminal-titlebar"><div class="terminal-dot" style="background:#ff5f56"></div><div class="terminal-dot" style="background:#ffbd2e"></div><div class="terminal-dot" style="background:#27c93f"></div><span style="margin-left:12px;color:var(--text-dim);font-size:12px">ruhi-core@engine ~ $</span></div><div class="terminal-body" id="termBody"><div class="terminal-output">Welcome to RUHI-CORE Terminal β‘\\n\\nType commands below. Use "clear" to clear.\\n</div></div><div class="terminal-input-row"><span class="terminal-prompt">β―</span><input class="terminal-input" id="termInput" placeholder="Enter command..." onkeydown="if(event.key==='Enter')execTerm()"></div></div> | |
| </div> | |
| <!-- ====== METRICS PAGE ====== --> | |
| <div class="page" id="page-metrics"> | |
| <div class="page-header"><div class="page-title">System Metrics</div><div class="page-desc">Detailed performance analytics</div></div> | |
| <div class="stats-grid" id="metricsGrid"></div> | |
| <div class="form-panel"><h3>ποΈ Process Tree</h3><div id="procTree" style="font-family:'JetBrains Mono',monospace;font-size:12px;line-height:1.9;color:var(--text-secondary)">Loading...</div></div> | |
| </div> | |
| <!-- ====== SETTINGS PAGE ====== --> | |
| <div class="page" id="page-settings"> | |
| <div class="page-header"><div class="page-title">Settings</div><div class="page-desc">Platform configuration</div></div> | |
| <div class="form-panel"> | |
| <h3>βοΈ System Info</h3> | |
| <div id="settingsInfo" style="font-family:'JetBrains Mono',monospace;font-size:13px;line-height:2;color:var(--text-secondary)">Loading...</div> | |
| </div> | |
| <div class="form-panel"> | |
| <h3>π§ Quick Actions</h3> | |
| <div style="display:flex;gap:8px;flex-wrap:wrap"> | |
| <button class="btn btn-success" onclick="apiCall('/api/services/start-all','POST').then(()=>{toast('All services starting...','success');loadSvc()})">βΆ Start All</button> | |
| <button class="btn btn-danger" onclick="if(confirm('Stop all?'))apiCall('/api/services/stop-all','POST').then(()=>{toast('All stopped','info');loadSvc()})">βΉ Stop All</button> | |
| <button class="btn" onclick="refreshAll()">π Refresh All</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div><!-- page-content --> | |
| </div><!-- main-area --> | |
| </div><!-- app --> | |
| <script> | |
| // ============================================ | |
| // π₯ RUHI-CORE Dashboard Engine | |
| // ============================================ | |
| let T=localStorage.getItem('ruhi_t')||'',WS=null,SVC=[],curPath='',editPath=''; | |
| // Init | |
| window.onload=()=>{ | |
| if(T)verify(); | |
| tick();setInterval(tick,1000); | |
| byId('loginPass').onkeydown=e=>{if(e.key==='Enter')doLogin()}; | |
| // Drag drop | |
| const dz=byId('dropZone'); | |
| if(dz){dz.ondragover=e=>{e.preventDefault();dz.classList.add('active')};dz.ondragleave=()=>dz.classList.remove('active');dz.ondrop=e=>{e.preventDefault();dz.classList.remove('active');if(e.dataTransfer.files[0])handleDeploy(e.dataTransfer.files[0])}} | |
| // Responsive | |
| checkMobile();window.onresize=checkMobile; | |
| }; | |
| function byId(id){return document.getElementById(id)} | |
| function tick(){const n=new Date();byId('clock')&&(byId('clock').textContent=n.toLocaleTimeString())} | |
| function checkMobile(){const m=window.innerWidth<=1024;byId('menuBtn')&&(byId('menuBtn').style.display=m?'flex':'none');if(m)byId('sidebar')?.classList.remove('open')} | |
| function toggleSidebar(){byId('sidebar').classList.toggle('open')} | |
| function esc(t){const d=document.createElement('div');d.textContent=t;return d.innerHTML} | |
| // Auth | |
| async function doLogin(){ | |
| const u=byId('loginUser').value.trim(),p=byId('loginPass').value.trim(),e=byId('loginError'); | |
| if(!u||!p){e.textContent='Enter credentials';e.style.display='block';return} | |
| try{const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})});const d=await r.json();if(r.ok){T=d.access_token;localStorage.setItem('ruhi_t',T);enter()}else{e.textContent=d.detail||'Failed';e.style.display='block'}}catch(x){e.textContent='Error: '+x.message;e.style.display='block'} | |
| } | |
| async function verify(){try{const r=await fetch('/api/auth/verify',{headers:{'Authorization':'Bearer '+T}});const d=await r.json();if(d.valid)enter();else logout()}catch{logout()}} | |
| function enter(){byId('loginScreen').style.display='none';byId('appContainer').classList.add('active');loadAll();connectWS();browseFM('')} | |
| function logout(){T='';localStorage.removeItem('ruhi_t');WS&&WS.close();byId('appContainer').classList.remove('active');byId('loginScreen').style.display='flex'} | |
| function doLogout(){if(confirm('Logout?'))logout()} | |
| // API | |
| async function apiCall(url,method='GET',body=null){const o={method,headers:{'Authorization':'Bearer '+T,'Content-Type':'application/json'}};if(body)o.body=JSON.stringify(body);const r=await fetch(url,o);if(r.status===401){logout();return null}return r.json()} | |
| // WebSocket | |
| function connectWS(){const p=location.protocol==='https:'?'wss:':'ws:';WS=new WebSocket(p+'//'+location.host+'/ws/dashboard?token='+T);WS.onopen=()=>{byId('wsDot').className='status-dot';byId('wsLabel').textContent='Live';byId('wsLabel').style.color='var(--success)';byId('statusDot').className='status-dot'};WS.onclose=()=>{byId('wsDot').className='status-dot warning';byId('wsLabel').textContent='Reconnecting';byId('wsLabel').style.color='var(--warning)';setTimeout(connectWS,3000)};WS.onerror=()=>{byId('wsDot').className='status-dot offline';byId('wsLabel').textContent='Error'}} | |
| // Navigation | |
| function go(page,el){ | |
| document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); | |
| document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active')); | |
| byId('page-'+page)?.classList.add('active'); | |
| if(el)el.classList.add('active'); | |
| byId('pageLabel').textContent=page.charAt(0).toUpperCase()+page.slice(1); | |
| if(page==='dashboard')loadAll(); | |
| if(page==='services')loadSvc(); | |
| if(page==='metrics')loadMetrics(); | |
| if(page==='files')browseFM(curPath); | |
| if(page==='settings')loadSettings(); | |
| if(window.innerWidth<=1024)byId('sidebar').classList.remove('open'); | |
| } | |
| // Load All | |
| async function loadAll(){await loadSvc();await loadMetrics()} | |
| async function refreshAll(){await loadAll();toast('Refreshed!','info')} | |
| // Services | |
| async function loadSvc(){ | |
| const d=await apiCall('/api/services/');if(!d)return; | |
| SVC=d.services||[];byId('svcCount').textContent=SVC.length; | |
| renderSvc();updateLogSel(); | |
| } | |
| function renderSvc(){ | |
| const html=SVC.length?SVC.map(s=>` | |
| <div class="svc-card ${s.status} animate-in"> | |
| <div class="svc-header"> | |
| <div class="svc-name"><span class="svc-type-icon">${s.type==='bot'?'π€':s.type==='worker'?'βοΈ':'π'}</span>${s.name}</div> | |
| <div class="svc-badge ${s.status}">${s.status}</div> | |
| </div> | |
| <div class="svc-info"> | |
| <div class="svc-info-item"><div class="svc-info-label">Type</div><div class="svc-info-value">${s.type}</div></div> | |
| <div class="svc-info-item"><div class="svc-info-label">Language</div><div class="svc-info-value">${s.language}</div></div> | |
| <div class="svc-info-item"><div class="svc-info-label">PID</div><div class="svc-info-value">${s.pid||'-'}</div></div> | |
| <div class="svc-info-item"><div class="svc-info-label">Port</div><div class="svc-info-value">${s.port||'-'}</div></div> | |
| <div class="svc-info-item"><div class="svc-info-label">CPU</div><div class="svc-info-value">${s.cpu_percent}%</div></div> | |
| <div class="svc-info-item"><div class="svc-info-label">Memory</div><div class="svc-info-value">${s.memory_mb}MB</div></div> | |
| <div class="svc-info-item"><div class="svc-info-label">Uptime</div><div class="svc-info-value">${s.uptime_formatted||'-'}</div></div> | |
| <div class="svc-info-item"><div class="svc-info-label">Restarts</div><div class="svc-info-value">${s.restart_count}</div></div> | |
| </div> | |
| <div class="svc-actions"> | |
| ${s.status!=='running'?`<button class="btn btn-success btn-sm" onclick="svcCtrl('${s.id}','start')">βΆ Start</button>`:`<button class="btn btn-danger btn-sm" onclick="svcCtrl('${s.id}','stop')">βΉ Stop</button>`} | |
| <button class="btn btn-warning btn-sm" onclick="svcCtrl('${s.id}','restart')">π</button> | |
| <button class="btn btn-sm" onclick="viewLogs('${s.id}')">π</button> | |
| <button class="btn btn-sm" onclick="browseFM('apps/${s.name}');go('files',document.querySelector('[data-page=files]'))">π</button> | |
| <button class="btn btn-danger btn-sm" onclick="delSvc('${s.id}','${s.name}')">ποΈ</button> | |
| </div> | |
| </div>`).join(''):'<div style="text-align:center;padding:60px;color:var(--text-dim);grid-column:1/-1"><div style="font-size:48px;margin-bottom:16px">π</div><h3 style="color:var(--text-secondary);margin-bottom:8px">No services yet</h3><p>Create your first service above!</p></div>'; | |
| byId('svcGrid').innerHTML=html;byId('dashSvcGrid').innerHTML=html; | |
| } | |
| async function createSvc(){const n=byId('svcName').value.trim();if(!n){toast('Enter name','error');return}const b={name:n,type:byId('svcType').value,language:byId('svcLang').value,entry_file:byId('svcEntry').value||'main.py',command:byId('svcCmd').value,description:byId('svcDesc').value,auto_restart:true};const d=await apiCall('/api/services/','POST',b);if(d&&!d.detail){toast('Service created! β‘','success');byId('svcName').value='';byId('svcCmd').value='';byId('svcDesc').value='';loadSvc()}else toast(d?.detail||'Failed','error')} | |
| async function svcCtrl(id,act){const d=await apiCall('/api/services/'+id+'/'+act,'POST');if(d){toast(d.message,d.success!==false?'success':'error');loadSvc()}} | |
| async function delSvc(id,name){if(!confirm('Delete "'+name+'"?'))return;await apiCall('/api/services/'+id,'DELETE');toast('Deleted','info');loadSvc()} | |
| // Metrics | |
| async function loadMetrics(){ | |
| const d=await apiCall('/api/metrics/summary');if(!d)return; | |
| const c=d.cpu,m=d.memory,dk=d.disk,n=d.network,sv=d.services,pr=d.processes,sys=d.system; | |
| const cC=c.usage_percent>80?'danger':c.usage_percent>60?'warning':'neon'; | |
| const mC=m.usage_percent>80?'danger':m.usage_percent>60?'warning':'purple'; | |
| const dC=dk.data_usage_percent>80?'danger':dk.data_usage_percent>60?'warning':'info'; | |
| byId('statsGrid').innerHTML=` | |
| <div class="stat-card animate-in-1"><div class="stat-header"><div class="stat-icon cpu">β‘</div></div><div class="stat-value">${c.usage_percent}<span class="stat-unit">%</span></div><div class="stat-label">CPU Usage</div><div class="progress-track"><div class="progress-bar ${cC}" style="width:${c.usage_percent}%"></div></div><div class="stat-sub">${c.cores_logical} cores</div></div> | |
| <div class="stat-card animate-in-2"><div class="stat-header"><div class="stat-icon mem">π§ </div></div><div class="stat-value">${m.usage_percent}<span class="stat-unit">%</span></div><div class="stat-label">Memory</div><div class="progress-track"><div class="progress-bar ${mC}" style="width:${m.usage_percent}%"></div></div><div class="stat-sub">${m.used_mb}MB / ${m.total_mb}MB</div></div> | |
| <div class="stat-card animate-in-3"><div class="stat-header"><div class="stat-icon disk">πΎ</div></div><div class="stat-value">${dk.data_usage_percent}<span class="stat-unit">%</span></div><div class="stat-label">Storage</div><div class="progress-track"><div class="progress-bar ${dC}" style="width:${dk.data_usage_percent}%"></div></div><div class="stat-sub">${dk.data_used_gb}GB / ${dk.data_total_gb}GB</div></div> | |
| <div class="stat-card animate-in-4"><div class="stat-header"><div class="stat-icon svc">π§</div></div><div class="stat-value">${sv.running}<span class="stat-unit">/ ${sv.total_services}</span></div><div class="stat-label">Services Running</div><div class="progress-track"><div class="progress-bar neon" style="width:${sv.total_services?sv.running/sv.total_services*100:0}%"></div></div><div class="stat-sub">${sv.crashed||0} crashed</div></div> | |
| <div class="stat-card animate-in-5"><div class="stat-header"><div class="stat-icon time">β±οΈ</div></div><div class="stat-value" style="font-size:22px">${sys.uptime||'0m'}</div><div class="stat-label">Uptime</div><div class="stat-sub">${sys.platform||''} β’ Python ${sys.python_version||''}</div></div> | |
| <div class="stat-card animate-in-6"><div class="stat-header"><div class="stat-icon net">π</div></div><div class="stat-value" style="font-size:20px">β${n.sent_rate_kbps} β${n.recv_rate_kbps}</div><div class="stat-label">Network (KB/s)</div><div class="stat-sub">${n.active_connections} connections</div></div> | |
| <div class="stat-card animate-in-7"><div class="stat-header"><div class="stat-icon proc">π</div></div><div class="stat-value">${pr.total}</div><div class="stat-label">Processes</div><div class="stat-sub">${pr.running} active, ${pr.zombie||0} zombie</div></div> | |
| <div class="stat-card animate-in"><div class="stat-header"><div class="stat-icon ws">π‘</div></div><div class="stat-value">${d.websockets?.total_connections||0}</div><div class="stat-label">WebSocket</div><div class="stat-sub">Live connections</div></div>`; | |
| // Detailed metrics | |
| const la=c.load_average||[0,0,0]; | |
| const br=dk.breakdown_mb||{};let bh=Object.entries(br).map(([k,v])=>`<div style="display:flex;justify-content:space-between;padding:3px 0"><span>${k}</span><span style="color:var(--neon)">${v}MB</span></div>`).join(''); | |
| byId('metricsGrid').innerHTML=` | |
| <div class="stat-card"><div class="stat-icon cpu" style="margin-bottom:12px">π</div><div class="stat-value" style="font-size:20px">${la.join(' / ')}</div><div class="stat-label">Load Average (1/5/15m)</div></div> | |
| <div class="stat-card"><div class="stat-icon net" style="margin-bottom:12px">π</div><div class="stat-value" style="font-size:20px">β${n.total_sent_mb}MB</div><div class="stat-label">Network Total</div><div class="stat-sub">β${n.total_recv_mb}MB received</div></div> | |
| <div class="stat-card"><div class="stat-icon disk" style="margin-bottom:12px">πΎ</div><div class="stat-value">${dk.data_free_gb}GB</div><div class="stat-label">Free Space</div></div> | |
| <div class="stat-card"><div class="stat-label" style="margin-bottom:8px">Storage Breakdown</div><div style="font-size:12px;font-family:'JetBrains Mono',monospace">${bh||'N/A'}</div></div>`; | |
| byId('procTree').innerHTML=(pr.top||[]).map(p=>`<div style="display:flex;gap:16px;padding:2px 0"><span style="color:var(--text-dim);min-width:60px">PID ${p.pid}</span><span style="min-width:180px">${p.name||'?'}</span><span style="color:var(--neon);min-width:70px">CPU ${p.cpu}%</span><span style="color:var(--purple)">MEM ${p.memory}%</span></div>`).join('')||'No data'; | |
| } | |
| async function loadSettings(){ | |
| const d=await apiCall('/api/metrics/summary');if(!d)return; | |
| byId('settingsInfo').innerHTML=` | |
| <div>Platform: <span style="color:var(--neon)">RUHI-CORE v1.0.0</span></div> | |
| <div>Host: <span style="color:var(--text-primary)">${d.system?.hostname||'N/A'}</span></div> | |
| <div>OS: <span style="color:var(--text-primary)">${d.system?.platform||'N/A'}</span></div> | |
| <div>Python: <span style="color:var(--text-primary)">${d.system?.python_version||'N/A'}</span></div> | |
| <div>CPU Cores: <span style="color:var(--text-primary)">${d.cpu?.cores_logical||0}</span></div> | |
| <div>Total RAM: <span style="color:var(--text-primary)">${d.memory?.total_mb||0}MB</span></div> | |
| <div>Total Disk: <span style="color:var(--text-primary)">${d.disk?.data_total_gb||0}GB</span></div> | |
| <div>Services: <span style="color:var(--text-primary)">${d.services?.total_services||0}</span></div> | |
| <div>Data Dir: <span style="color:var(--text-primary)">/data</span></div>`; | |
| } | |
| // File Manager | |
| async function browseFM(path){ | |
| curPath=path||''; | |
| const d=await apiCall('/api/files/browse?path='+encodeURIComponent(curPath)+'&show_hidden=false');if(!d)return; | |
| byId('fmBread').innerHTML=d.breadcrumb.map((b,i)=>i<d.breadcrumb.length-1?`<a onclick="browseFM('${b.path}')">${b.name}</a><span class="sep">/</span>`:`<span class="current-dir">${b.name}</span>`).join(''); | |
| let rows=''; | |
| if(curPath){const pp=curPath.split('/').slice(0,-1).join('/');rows+=`<tr><td><span class="fm-name" onclick="browseFM('${pp}')"><span class="fm-icon">β¬οΈ</span>..</span></td><td>-</td><td>-</td><td>-</td><td>-</td></tr>`} | |
| rows+=d.items.map(i=>`<tr> | |
| <td><span class="fm-name" onclick="${i.is_dir?`browseFM('${i.path}')`:i.editable?`openED('${i.path}')`:`toast('Cannot edit binary','info')`}"><span class="fm-icon">${i.icon}</span>${i.name}</span></td> | |
| <td class="fm-size">${i.size_formatted}</td><td class="fm-date">${i.modified_formatted||'-'}</td><td class="fm-size">${i.permissions||'-'}</td> | |
| <td class="fm-actions">${i.editable?`<button class="fm-action" onclick="openED('${i.path}')" title="Edit">βοΈ</button>`:''}<button class="fm-action" onclick="renameFM('${i.path}','${i.name}')" title="Rename">π</button><button class="fm-action del" onclick="deleteFM('${i.path}','${i.name}')" title="Delete">ποΈ</button></td> | |
| </tr>`).join(''); | |
| byId('fmBody').innerHTML=rows||'<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-dim)">Empty</td></tr>'; | |
| byId('fmInfo').textContent=`${d.summary.files} files, ${d.summary.directories} folders β’ ${d.summary.total_size_formatted}`; | |
| } | |
| async function openED(path){ | |
| const d=await apiCall('/api/files/read?path='+encodeURIComponent(path));if(!d||!d.editable){toast(d?.error||'Cannot edit','error');return} | |
| editPath=path;byId('edIcon').textContent=d.icon;byId('edName').textContent=d.name;byId('edLang').textContent=d.language;byId('edMeta').textContent=d.line_count+' lines β’ '+d.size_formatted;byId('edArea').value=d.content; | |
| go('editor',document.querySelector('[data-page=editor]')); | |
| } | |
| async function saveED(){if(!editPath)return;const d=await apiCall('/api/files/write','POST',{path:editPath,content:byId('edArea').value});if(d&&!d.detail){toast('Saved! '+d.size_formatted,'success');byId('edMeta').textContent='β Saved β’ '+d.size_formatted}else toast(d?.detail||'Failed','error')} | |
| function closeED(){editPath='';byId('edArea').value='';byId('edName').textContent='No file';byId('edMeta').textContent=''} | |
| async function deleteFM(p,n){if(!confirm('Delete "'+n+'"?'))return;await apiCall('/api/files/delete?path='+encodeURIComponent(p),'DELETE');toast('Deleted','success');browseFM(curPath)} | |
| async function renameFM(p,n){const nn=prompt('New name:',n);if(!nn||nn===n)return;const d=await apiCall('/api/files/rename','POST',{path:p,new_name:nn});if(d&&!d.detail){toast('Renamed','success');browseFM(curPath)}else toast(d?.detail||'Failed','error')} | |
| function newFileFM(){const n=prompt('File name:','untitled.py');if(!n)return;const p=curPath?curPath+'/'+n:n;apiCall('/api/files/create','POST',{path:p,content:'',is_directory:false}).then(d=>{if(d&&!d.detail){toast('Created','success');browseFM(curPath)}else toast(d?.detail||'Failed','error')})} | |
| function newDirFM(){const n=prompt('Folder name:');if(!n)return;const p=curPath?curPath+'/'+n:n;apiCall('/api/files/create','POST',{path:p,is_directory:true}).then(d=>{if(d&&!d.detail){toast('Created','success');browseFM(curPath)}else toast(d?.detail||'Failed','error')})} | |
| function uploadFM(){const i=document.createElement('input');i.type='file';i.multiple=true;i.onchange=async()=>{for(const f of i.files){const fd=new FormData();fd.append('file',f);fd.append('path',curPath||'uploads');const r=await fetch('/api/files/upload',{method:'POST',headers:{'Authorization':'Bearer '+T},body:fd});if(r.ok)toast('Uploaded: '+f.name,'success');else toast('Failed: '+f.name,'error')}browseFM(curPath)};i.click()} | |
| async function searchFM(){const q=byId('fmSearch').value.trim();if(!q){browseFM(curPath);return}const d=await apiCall('/api/files/search?query='+encodeURIComponent(q)+'&path='+curPath);if(!d)return;byId('fmBody').innerHTML=d.results.length?d.results.map(i=>`<tr><td><span class="fm-name" onclick="${i.is_dir?`browseFM('${i.path}')`:i.editable?`openED('${i.path}')`:``}"><span class="fm-icon">${i.icon}</span>${i.name}<div style="font-size:10px;color:var(--text-dim)">${i.path}</div></span></td><td class="fm-size">${i.size_formatted}</td><td>-</td><td>-</td><td>-</td></tr>`).join(''):'<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-dim)">No results</td></tr>';byId('fmInfo').textContent=d.total_found+' results'} | |
| async function diskUsageFM(){const d=await apiCall('/api/files/disk-usage?path='+encodeURIComponent(curPath));if(!d)return;let h=`<h3>πΎ Disk Usage</h3><div style="margin:16px 0"><div style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px"><span>Used: ${d.disk.used_formatted}</span><span>Free: ${d.disk.free_formatted}</span></div><div class="progress-track" style="height:10px"><div class="progress-bar ${d.disk.percent>80?'danger':d.disk.percent>60?'warning':'neon'}" style="width:${d.disk.percent}%"></div></div><div style="text-align:center;margin-top:6px;font-size:11px;color:var(--text-dim)">${d.disk.percent}% of ${d.disk.total_formatted}</div></div><div style="margin-top:16px">`;for(const[n,i]of Object.entries(d.directories||{}))h+=`<div style="display:flex;justify-content:space-between;padding:5px 0;font-size:13px;border-bottom:1px solid var(--border)"><span>π ${n}</span><span style="color:var(--neon);font-family:'JetBrains Mono',monospace">${i.size_formatted}</span></div>`;h+='</div>';showModal(h)} | |
| // Logs | |
| function updateLogSel(){const s=byId('logSelect');s.innerHTML='<option value="">Select service...</option>'+SVC.map(s=>`<option value="${s.id}">${s.name} (${s.status})</option>`).join('')} | |
| async function loadLogs(){const id=byId('logSelect').value;if(!id)return;const d=await apiCall('/api/services/'+id+'/logs?lines=200');if(!d)return;byId('logTitle').textContent='π '+d.service_name+' β’ '+d.returned_lines+' lines';byId('logBody').innerHTML=d.logs.map(l=>`<div class="log-line">${esc(l)}</div>`).join('');byId('logBody').scrollTop=byId('logBody').scrollHeight} | |
| function viewLogs(id){go('logs',document.querySelector('[data-page=logs]'));byId('logSelect').value=id;loadLogs()} | |
| // Terminal | |
| async function execTerm(){const i=byId('termInput'),cmd=i.value.trim();if(!cmd)return;const b=byId('termBody');if(cmd==='clear'){b.innerHTML='';i.value='';return}b.innerHTML+=`<div class="terminal-cmd">β― ${esc(cmd)}</div>`;i.value='';const d=await apiCall('/api/terminal/execute','POST',{command:cmd,cwd:'/data'});if(d){if(d.stdout)b.innerHTML+=`<div class="terminal-output">${esc(d.stdout)}</div>`;if(d.stderr)b.innerHTML+=`<div class="terminal-err">${esc(d.stderr)}</div>`;if(d.error)b.innerHTML+=`<div class="terminal-err">${esc(d.error)}</div>`}b.scrollTop=b.scrollHeight} | |
| // Deploy | |
| async function handleDeploy(file){if(!file)return;const steps=byId('deploySteps'),result=byId('deployResult');steps.style.display='block';result.style.display='none'; | |
| ['upload','extract','detect','deps','launch'].forEach(s=>{const el=byId('ds-'+s);el.className='deploy-step';el.querySelector('.step-bullet').textContent={'upload':'1','extract':'2','detect':'3','deps':'4','launch':'5'}[s]}); | |
| setDS('upload','active'); | |
| try{const fd=new FormData();fd.append('file',file);fd.append('service_name',byId('deployName').value.trim());fd.append('auto_start',byId('deployAuto').checked);setDS('upload','done');setDS('extract','active'); | |
| const r=await fetch('/api/files/deploy/upload',{method:'POST',headers:{'Authorization':'Bearer '+T},body:fd});const d=await r.json(); | |
| if(r.ok){['extract','detect','deps','launch'].forEach(s=>setDS(s,'done'));result.style.display='block';result.innerHTML=`<div class="glass-card" style="border-color:rgba(46,213,115,0.3)"><h3 style="color:var(--success);margin-bottom:12px">β Deployment Successful!</h3><div class="svc-info"><div class="svc-info-item"><div class="svc-info-label">Service</div><div class="svc-info-value">${d.service_name}</div></div><div class="svc-info-item"><div class="svc-info-label">Type</div><div class="svc-info-value">${d.type}</div></div><div class="svc-info-item"><div class="svc-info-label">Language</div><div class="svc-info-value">${d.language}</div></div><div class="svc-info-item"><div class="svc-info-label">Entry</div><div class="svc-info-value">${d.entry_file}</div></div><div class="svc-info-item"><div class="svc-info-label">Status</div><div class="svc-info-value">${d.status}</div></div><div class="svc-info-item"><div class="svc-info-label">ID</div><div class="svc-info-value">${d.service_id}</div></div></div>${d.deploy_log?'<div style="margin-top:12px;font-size:11px;color:var(--text-secondary);font-family:JetBrains Mono,monospace">'+d.deploy_log.map(l=>'<div>'+esc(l)+'</div>').join('')+'</div>':''}</div>`;toast('Deployed! π','success');loadSvc()} | |
| else throw new Error(d.detail||'Failed') | |
| }catch(e){['upload','extract','detect','deps','launch'].forEach(s=>{const el=byId('ds-'+s);if(el.classList.contains('active'))setDS(s,'error')});result.style.display='block';result.innerHTML=`<div class="glass-card" style="border-color:rgba(255,71,87,0.3)"><h3 style="color:var(--danger)">β Failed</h3><p style="margin-top:8px;color:var(--text-secondary)">${esc(e.message)}</p></div>`;toast('Deploy failed','error')}} | |
| function setDS(s,state){const el=byId('ds-'+s);el.className='deploy-step '+state;el.querySelector('.step-bullet').textContent=state==='done'?'β':state==='error'?'β':state==='active'?'β':{'upload':'1','extract':'2','detect':'3','deps':'4','launch':'5'}[s]} | |
| // Modal | |
| function showModal(h){byId('modalBox').innerHTML=h+'<div class="modal-footer"><button class="btn" onclick="closeModal()">Close</button></div>';byId('modalOverlay').classList.add('open')} | |
| function closeModal(){byId('modalOverlay').classList.remove('open')} | |
| // Toast | |
| function toast(msg,type='info'){const s=byId('toastStack'),t=document.createElement('div');const icons={success:'β ',error:'β',info:'π‘',warning:'β οΈ'};t.className='toast-item '+type;t.innerHTML=`<span class="toast-icon">${icons[type]||'π‘'}</span>${esc(msg)}`;s.appendChild(t);setTimeout(()=>{t.classList.add('removing');setTimeout(()=>t.remove(),300)},4000)} | |
| // Auto refresh | |
| setInterval(()=>{if(T&&byId('appContainer').classList.contains('active'))loadSvc()},12000); | |
| </script> | |
| </body> | |
| </html>''' | |