Spaces:
Paused
Paused
Download index.html from mohammed978/wolf-host: direct link, hf CLI and curl.
- Browser
- Download file 39.2 kB
-
https://huggingface.co/spaces/mohammed978/wolf-host/resolve/main/index.html
- Command line
-
hf download hf://spaces/mohammed978/wolf-host/index.html
-
curl -L -o index.html https://huggingface.co/spaces/mohammed978/wolf-host/resolve/main/index.html
39.2 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <link rel="icon" type="image/png" href="/logo.png" /> | |
| <title>Wolf Host</title> | |
| <style> | |
| :root{ | |
| --bg0:#05060a;--bg1:#070a12; | |
| --glass:rgba(255,255,255,.06); | |
| --border:rgba(255,255,255,.10); | |
| --muted:rgba(255,255,255,.65); | |
| --text:rgba(255,255,255,.92); | |
| --accent:#00d4ff;--accent2:#7b61ff; | |
| --danger:#ff4d4d;--ok:#33ff99;--warn:#ffd166; | |
| --shadow:0 18px 60px rgba(0,0,0,.55); | |
| --radius:18px; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{height:100%} | |
| body{ | |
| margin:0;color:var(--text); | |
| font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; | |
| background: | |
| radial-gradient(1200px 600px at 10% 10%, rgba(0,212,255,.25), transparent 60%), | |
| radial-gradient(900px 500px at 90% 20%, rgba(123,97,255,.22), transparent 55%), | |
| radial-gradient(800px 500px at 40% 90%, rgba(51,255,153,.10), transparent 60%), | |
| linear-gradient(180deg, var(--bg1), var(--bg0)); | |
| overflow-x:hidden; | |
| } | |
| body:before{ | |
| content:"";position:fixed;inset:0;pointer-events:none; | |
| background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.12'/%3E%3C/svg%3E"); | |
| opacity:.22;mix-blend-mode:overlay; | |
| } | |
| .topbar{ | |
| position:sticky;top:0;z-index:20; | |
| backdrop-filter: blur(14px); | |
| -webkit-backdrop-filter: blur(14px); | |
| background: rgba(0,0,0,.35); | |
| border-bottom:1px solid var(--border); | |
| } | |
| .topbar-inner{ | |
| max-width:1200px;margin:0 auto; | |
| padding:14px 18px; | |
| display:flex;gap:12px; | |
| align-items:center;justify-content:space-between; | |
| } | |
| .brand{ | |
| display:flex;align-items:center;gap:12px; | |
| font-weight:900;letter-spacing:.8px;text-transform:uppercase; | |
| } | |
| .brand .logo{ | |
| width:38px;height:38px;border-radius:12px; | |
| cursor:pointer;object-fit:cover; | |
| background: | |
| radial-gradient(circle at 30% 30%, rgba(0,212,255,.9), transparent 60%), | |
| radial-gradient(circle at 70% 70%, rgba(123,97,255,.7), transparent 55%), | |
| rgba(255,255,255,.05); | |
| border:1px solid rgba(255,255,255,.14); | |
| box-shadow: 0 10px 30px rgba(0,212,255,.15); | |
| display:grid;place-items:center; | |
| } | |
| .brand .title{color:var(--accent);font-size:15px;line-height:1.1} | |
| .brand .sub{ | |
| display:block;font-weight:700;color:rgba(255,255,255,.6); | |
| font-size:11px;letter-spacing:.2px;margin-top:2px;text-transform:none | |
| } | |
| .actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap} | |
| .btn{ | |
| border:1px solid var(--border); | |
| background: rgba(255,255,255,.06); | |
| color: var(--text); | |
| padding:10px 14px; | |
| border-radius: 12px; | |
| cursor:pointer; | |
| font-weight:800; | |
| font-size:12px; | |
| transition: transform .12s ease, background .12s ease, border-color .12s ease; | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| } | |
| .btn:hover{transform: translateY(-1px); border-color: rgba(255,255,255,.18); background: rgba(255,255,255,.09);} | |
| .btn-primary{ | |
| background: linear-gradient(135deg, rgba(0,212,255,.95), rgba(123,97,255,.85)); | |
| color:#00131c; | |
| border:0; | |
| box-shadow: 0 14px 40px rgba(0,212,255,.18); | |
| } | |
| .btn-danger{ | |
| background: rgba(255,77,77,.12); | |
| border-color: rgba(255,77,77,.25); | |
| color: rgba(255,220,220,.95); | |
| } | |
| .btn-mini{padding:7px 10px;border-radius:10px;font-size:11px} | |
| .container{max-width:1200px;margin:20px auto 60px;padding:0 18px} | |
| .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px} | |
| @media (max-width:1050px){.grid{grid-template-columns:repeat(3,1fr)}} | |
| @media (max-width:760px){.grid{grid-template-columns:repeat(2,1fr)}} | |
| @media (max-width:480px){.grid{grid-template-columns:1fr}} | |
| .card{ | |
| border:1px solid var(--border); | |
| background: linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.03)); | |
| border-radius: var(--radius); | |
| padding:16px; | |
| box-shadow: var(--shadow); | |
| backdrop-filter: blur(14px); | |
| -webkit-backdrop-filter: blur(14px); | |
| cursor:pointer; | |
| transition: transform .15s ease, border-color .15s ease, background .15s ease; | |
| position:relative; | |
| overflow:hidden; | |
| min-height: 120px; | |
| } | |
| .card:hover{transform: translateY(-3px);border-color: rgba(0,212,255,.35)} | |
| .srv-title{font-weight:950;letter-spacing:.6px;text-transform:uppercase;font-size:14px;margin:0;padding-right:34px} | |
| .card-del{position:absolute;top:10px;right:10px;z-index:3;opacity:.85} | |
| .card-del:hover{opacity:1} | |
| .srv-sub{margin-top:6px;color:var(--muted);font-size:12px;line-height:1.35} | |
| .srv-site{display:inline-block;margin-top:8px;font-size:12px;color:var(--accent);text-decoration:none} | |
| .pill{ | |
| display:inline-flex;align-items:center;gap:6px; | |
| padding:5px 10px;border-radius:999px; | |
| border:1px solid var(--border); | |
| background: rgba(0,0,0,.18); | |
| font-size:11px;font-weight:900; | |
| letter-spacing:.4px;margin-top:10px; | |
| text-transform:uppercase; | |
| } | |
| .dot{width:8px;height:8px;border-radius:50%} | |
| .pill.running .dot{background:var(--ok)} | |
| .pill.installing .dot{background:var(--warn)} | |
| .pill.banned .dot{background:var(--danger)} | |
| .pill.offline .dot{background:rgba(255,255,255,.35)} | |
| #serverView{display:none} | |
| .header-row{ | |
| display:flex;align-items:flex-start;justify-content:space-between; | |
| gap:12px;flex-wrap:wrap;margin-top:10px | |
| } | |
| .hleft h1{margin:0;font-size:22px;font-weight:950;letter-spacing:.8px;text-transform:uppercase} | |
| .hleft .muted{margin-top:6px;color:var(--muted);font-size:12px} | |
| .tabs{display:flex;gap:10px;align-items:center} | |
| .tab{ | |
| border:1px solid var(--border); | |
| background: rgba(255,255,255,.06); | |
| color: var(--text); | |
| padding:10px 14px;border-radius: 12px; | |
| cursor:pointer;font-weight:900;font-size:12px; | |
| } | |
| .tab.active{ | |
| background: linear-gradient(135deg, rgba(0,212,255,.95), rgba(123,97,255,.85)); | |
| color:#00131c;border:0; | |
| box-shadow: 0 14px 40px rgba(0,212,255,.18); | |
| } | |
| .panel{ | |
| margin-top:14px; | |
| border:1px solid var(--border); | |
| background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03)); | |
| border-radius: var(--radius); | |
| backdrop-filter: blur(14px); | |
| -webkit-backdrop-filter: blur(14px); | |
| box-shadow: var(--shadow); | |
| overflow:hidden; | |
| } | |
| .grid-2{display:grid;grid-template-columns: 1fr 340px;gap:14px;padding:14px} | |
| @media (max-width:980px){.grid-2{grid-template-columns:1fr}} | |
| .console{ | |
| background: rgba(0,0,0,.45); | |
| border:1px solid rgba(255,255,255,.10); | |
| border-radius: 16px; | |
| padding:14px; | |
| height: 560px; | |
| overflow:auto; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; | |
| font-size:13px; | |
| line-height:1.45; | |
| white-space:pre-wrap; | |
| color: rgba(235,245,255,.86); | |
| } | |
| .side{display:flex;flex-direction:column;gap:10px} | |
| .btn-block{ | |
| width:100%; | |
| padding:12px; | |
| border-radius: 14px; | |
| border:1px solid rgba(255,255,255,.12); | |
| background: rgba(255,255,255,.06); | |
| color: var(--text); | |
| font-weight:950; | |
| cursor:pointer; | |
| transition: .12s ease; | |
| } | |
| .btn-block:hover{transform: translateY(-1px); background: rgba(255,255,255,.09); border-color: rgba(255,255,255,.18)} | |
| .btn-block.primary{ | |
| background: linear-gradient(135deg, rgba(0,212,255,.95), rgba(123,97,255,.85)); | |
| color:#00131c; border:0; | |
| } | |
| .btn-block.danger{ | |
| background: rgba(255,77,77,.12); | |
| border-color: rgba(255,77,77,.25); | |
| color: rgba(255,220,220,.95); | |
| } | |
| .stat{ | |
| border:1px solid rgba(255,255,255,.10); | |
| background: rgba(0,0,0,.22); | |
| border-radius: 16px; | |
| padding:12px; | |
| display:flex; | |
| gap:10px; | |
| align-items:center; | |
| } | |
| .stat .ico{ | |
| width:36px;height:36px;border-radius:14px; | |
| display:grid;place-items:center; | |
| background:rgba(255,255,255,.06); | |
| border:1px solid rgba(255,255,255,.10) | |
| } | |
| .stat small{display:block;color:var(--muted);font-weight:800;letter-spacing:.4px} | |
| .stat b{display:block;margin-top:4px;font-size:13px} | |
| #mainFileTxt{color:var(--accent);cursor:pointer} | |
| .fm-top{ | |
| display:flex;align-items:flex-start;justify-content:space-between; | |
| gap:12px;flex-wrap:wrap; | |
| padding:14px;border-bottom:1px solid rgba(255,255,255,.08) | |
| } | |
| .path{color:var(--muted);font-size:12px;margin-top:6px} | |
| .crumb a{color:var(--accent);text-decoration:none;font-weight:900} | |
| .crumb .sep{color:rgba(255,255,255,.35);padding:0 6px} | |
| .table{width:100%;border-collapse:collapse} | |
| .table th,.table td{padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.08);font-size:13px} | |
| .table th{color:rgba(255,255,255,.75);font-weight:950;text-transform:uppercase;letter-spacing:.5px;font-size:11px} | |
| .name-link{color:var(--accent);font-weight:900;cursor:pointer} | |
| .row-actions{white-space:nowrap} | |
| .modal-backdrop{ | |
| position:fixed; inset:0; | |
| background: rgba(0,0,0,.75); | |
| display:none; | |
| align-items:center; justify-content:center; | |
| z-index:50; | |
| padding:18px; | |
| } | |
| .modal{ | |
| width:100%; | |
| max-width:420px; | |
| background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.04)); | |
| border:1px solid rgba(255,255,255,.14); | |
| border-radius: 18px; | |
| box-shadow: var(--shadow); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| padding:16px; | |
| } | |
| .modal h3{margin:0 0 10px 0} | |
| .modal input{ | |
| width:100%; | |
| padding:12px; | |
| border-radius: 14px; | |
| border:1px solid rgba(255,255,255,.12); | |
| background: rgba(0,0,0,.35); | |
| color: var(--text); | |
| outline:none; | |
| } | |
| #editor-modal{ | |
| display:none; | |
| position:fixed; inset:0; | |
| background: rgba(0,0,0,.82); | |
| z-index:60; | |
| padding:18px; | |
| } | |
| .editor{ | |
| height:100%; | |
| max-width:1200px; | |
| margin:0 auto; | |
| border-radius: 18px; | |
| border:1px solid rgba(255,255,255,.14); | |
| background: rgba(10,14,22,.7); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| box-shadow: var(--shadow); | |
| display:flex; | |
| flex-direction:column; | |
| overflow:hidden; | |
| } | |
| .editor-top{ | |
| padding:12px 14px; | |
| border-bottom:1px solid rgba(255,255,255,.10); | |
| display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; | |
| } | |
| .editor-top .fn{font-weight:950;color:var(--accent);word-break:break-all} | |
| #ed-area{ | |
| flex:1; | |
| background: transparent; | |
| color: rgba(235,245,255,.9); | |
| border:0; | |
| outline:none; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; | |
| font-size:13px; | |
| padding:14px; | |
| resize:none; | |
| line-height:1.5; | |
| } | |
| /* ---- polish pack ---- */ | |
| @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.8)}} | |
| .pill.running .dot{animation:pulse 1.6s ease-in-out infinite;box-shadow:0 0 10px var(--ok)} | |
| .pill.installing .dot{animation:pulse 1s ease-in-out infinite;box-shadow:0 0 10px var(--warn)} | |
| .card::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;border-radius:var(--radius) var(--radius) 0 0; | |
| background:linear-gradient(90deg,var(--accent),var(--accent2));opacity:0;transition:opacity .15s} | |
| .card:hover::before{opacity:1} | |
| .btn-block:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none} | |
| .console-wrap{min-width:0;border:1px solid rgba(255,255,255,.10);border-radius:16px;overflow:hidden;background:rgba(0,0,0,.45)} | |
| .console-bar{display:flex;align-items:center;justify-content:space-between;gap:10px; | |
| padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.08);font-size:11px;font-weight:900;letter-spacing:.8px;color:var(--muted)} | |
| .live{display:inline-flex;align-items:center;gap:7px} | |
| .live-dot{width:8px;height:8px;border-radius:50%;background:var(--ok);animation:pulse 1.6s infinite;box-shadow:0 0 8px var(--ok)} | |
| .console{border:0;border-radius:0;background:transparent} | |
| .console::-webkit-scrollbar,.table-wrap::-webkit-scrollbar,.editor ::-webkit-scrollbar{height:8px;width:8px} | |
| .console::-webkit-scrollbar-thumb,.table-wrap::-webkit-scrollbar-thumb,.editor ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:8px} | |
| .empty{grid-column:1/-1;text-align:center;padding:56px 20px;border:1px dashed rgba(255,255,255,.2);border-radius:var(--radius); | |
| background:rgba(255,255,255,.03);color:var(--muted);line-height:1.8} | |
| .empty .big{font-size:42px;margin-bottom:8px} | |
| .empty b{color:var(--text)} | |
| .table-wrap{overflow-x:auto} | |
| #toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(20px);opacity:0;pointer-events:none; | |
| background:rgba(10,16,26,.94);border:1px solid rgba(0,212,255,.4);color:var(--text); | |
| padding:12px 18px;border-radius:14px;font-size:13px;font-weight:800;z-index:100; | |
| transition:opacity .25s,transform .25s;box-shadow:0 14px 40px rgba(0,0,0,.5);max-width:90vw} | |
| #toast.show{opacity:1;transform:translateX(-50%) translateY(0)} | |
| .stat{transition:border-color .15s,transform .15s} | |
| .stat:hover{border-color:rgba(0,212,255,.35);transform:translateY(-1px)} | |
| .stat b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="topbar"> | |
| <div class="topbar-inner"> | |
| <div class="brand"> | |
| <img class="logo" src="/logo.png" alt="3MH" role="button" tabindex="0" title="Home" | |
| onclick="location.href='/'" | |
| onkeypress="if(event.key==='Enter') location.href='/'"> | |
| <div> | |
| <div class="title">Wolf Host</div> | |
| <span class="sub" id="pyHostTxt">Python Host: loading...</span> | |
| </div> | |
| </div> | |
| <div class="actions"> | |
| <button id="adminBtn" class="btn" style="display:none" onclick="location.href='/admin'">Admin</button> | |
| <button class="btn" onclick="location.href='/docs'">Docs</button> | |
| <button id="homeBtn" class="btn" style="display:none" onclick="goHome()">Home</button> | |
| <button id="newBtn" class="btn btn-primary">+ New Server</button> | |
| <button class="btn btn-danger" onclick="location.href='/logout'">Logout</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="container" id="homeView"> | |
| <div class="grid" id="cardsContainer"></div> | |
| </div> | |
| <div class="container" id="serverView"> | |
| <div class="header-row"> | |
| <div class="hleft"> | |
| <h1 id="srvHeader">SERVER</h1> | |
| <div class="muted" id="srvSub">Manage your server</div> | |
| </div> | |
| <div class="tabs"> | |
| <button class="tab active" id="tabConsole" onclick="switchTab('console')">Console</button> | |
| <button class="tab" id="tabFiles" onclick="switchTab('files')">File Manager</button> | |
| </div> | |
| </div> | |
| <div class="panel" id="viewConsole"> | |
| <div class="grid-2"> | |
| <div class="console-wrap"> | |
| <div class="console-bar"> | |
| <span class="live"><span class="live-dot"></span>CONSOLE · LIVE</span> | |
| <button class="btn btn-mini" onclick="copyLogs()">Copy Output</button> | |
| </div> | |
| <div class="console" id="logOut">Initializing...</div> | |
| </div> | |
| <div class="side"> | |
| <button id="btnStart" class="btn-block primary" onclick="srvAction('start')">START</button> | |
| <button class="btn-block" onclick="srvAction('restart')">RESTART</button> | |
| <button class="btn-block danger" onclick="srvAction('stop')">STOP</button> | |
| <div class="stat"><div class="ico">⚡</div><div><small>STATUS</small><b id="statusTxt">Offline</b></div></div> | |
| <div class="stat"><div class="ico">🧠</div><div><small>RAM</small><b id="memTxt">0 MB</b></div></div> | |
| <div class="stat"><div class="ico">💿</div><div><small>MAIN FILE</small><b id="mainFileTxt" onclick="changeStartup()">Click to set</b></div></div> | |
| <div class="stat"><div class="ico">📡</div><div><small>IP</small><b id="ipTxt">0.0.0.0</b></div></div> | |
| <div class="stat" id="siteRow" style="display:none;cursor:pointer" onclick="openSite()"> | |
| <div class="ico">🌐</div><div><small>SITE (DIRECT)</small><b id="siteTxt">Open site</b></div> | |
| </div> | |
| <div class="stat" id="webhookRow" style="display:none;cursor:pointer" onclick="toggleWebhook()"> | |
| <div class="ico">🔗</div><div><small>TELEGRAM WEBHOOK</small><b id="webhookTxt">OFF · tap to enable</b></div> | |
| </div> | |
| <button class="btn-block danger" onclick="deleteServer()">🗑 DELETE SERVER</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel" id="viewFiles" style="display:none"> | |
| <div class="fm-top"> | |
| <div> | |
| <div style="font-weight:950;font-size:16px">File Manager</div> | |
| <div class="path" id="pathBar"></div> | |
| </div> | |
| <div class="actions"> | |
| <button id="btnBack" class="btn btn-mini" onclick="goUp()" style="display:none">⬅ Back</button> | |
| <button class="btn btn-mini" onclick="newFolder()">📁 Create Folder</button> | |
| <button class="btn btn-mini btn-primary" onclick="document.getElementById('uplFiles').click()">Upload Files</button> | |
| <button class="btn btn-mini" onclick="document.getElementById('uplFolder').click()">Upload Folder</button> | |
| </div> | |
| </div> | |
| <input type="file" id="uplFiles" multiple hidden onchange="doUploadFiles(this)"> | |
| <input type="file" id="uplFolder" webkitdirectory directory multiple hidden onchange="doUploadFolder(this)"> | |
| <div class="table-wrap"> | |
| <table class="table"> | |
| <thead><tr><th>Name</th><th>Size</th><th>Actions</th></tr></thead> | |
| <tbody id="fileList"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="editor-modal"> | |
| <div class="editor"> | |
| <div class="editor-top"> | |
| <div class="fn" id="ed-title">file</div> | |
| <div class="actions"> | |
| <button class="btn btn-primary" onclick="saveFile()">Save</button> | |
| <button class="btn btn-danger" onclick="closeEditor()">Exit</button> | |
| </div> | |
| </div> | |
| <textarea id="ed-area" spellcheck="false"></textarea> | |
| </div> | |
| </div> | |
| <div id="modalBackdrop" class="modal-backdrop"> | |
| <div class="modal"> | |
| <h3 id="mTitle">Title</h3> | |
| <input id="mIn" /> | |
| <div class="actions" style="justify-content:flex-end;margin-top:12px"> | |
| <button class="btn" onclick="closeM()">Cancel</button> | |
| <button id="mConfirm" class="btn btn-primary">Confirm</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="newServerBackdrop" class="modal-backdrop"> | |
| <div class="modal"> | |
| <h3>New Server</h3> | |
| <input id="nsName" placeholder="Name" /> | |
| <select id="nsType" style="width:100%;margin-top:10px;padding:10px;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:rgba(0,0,0,.4);color:inherit"> | |
| <option value="python">Python (bot / script)</option> | |
| <option value="webapp">Web App (Python)</option> | |
| <option value="api">API (Python)</option> | |
| <option value="php">PHP (full PHP runtime)</option> | |
| </select> | |
| <input id="nsSub" placeholder="Subdomain (for web app/API/PHP) — optional" style="margin-top:10px" /> | |
| <div class="actions" style="justify-content:flex-end;margin-top:12px"> | |
| <button class="btn" onclick="document.getElementById('newServerBackdrop').style.display='none'">Cancel</button> | |
| <button id="nsCreate" class="btn btn-primary">Create</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="toast"></div> | |
| <script> | |
| let currentSrv = null, poll = null, currentFile = null; | |
| let currentPath = ''; | |
| function isAuthPage(){ | |
| return location.pathname.startsWith('/login') || location.pathname.startsWith('/create'); | |
| } | |
| function safeRedirect(url){ | |
| if (!isAuthPage()) location.href = url; | |
| } | |
| function statusClass(st){ | |
| st = (st||'').toLowerCase(); | |
| if(st==='running') return 'running'; | |
| if(st==='installing' || st==='starting') return 'installing'; | |
| if(st==='banned') return 'banned'; | |
| return 'offline'; | |
| } | |
| async function loadPythonHost(){ | |
| const ipTxt = document.getElementById('pyHostTxt'); | |
| try{ | |
| const r = await fetch('/servers'); | |
| if (r.redirected){ safeRedirect(r.url); return; } | |
| const d = await r.json(); | |
| const port = location.port || 5000; | |
| if(!d.servers || d.servers.length === 0){ | |
| ipTxt.textContent = `Python Host: ${location.hostname}:${port}`; | |
| return; | |
| } | |
| const key = d.servers[0].key || d.servers[0].folder; | |
| const s = await fetch(`/server/stats/${encodeURIComponent(key)}`); | |
| if (s.redirected){ safeRedirect(s.url); return; } | |
| const sd = await s.json(); | |
| const ip = sd.ip || location.hostname; | |
| ipTxt.textContent = `Python Host: ${ip}:${port}`; | |
| }catch(e){ | |
| const port = location.port || 5000; | |
| ipTxt.textContent = `Python Host: ${location.hostname}:${port}`; | |
| } | |
| } | |
| async function loadServers() { | |
| const r = await fetch('/servers'); | |
| if (r.redirected){ safeRedirect(r.url); return; } | |
| const d = await r.json(); | |
| const cont = document.getElementById('cardsContainer'); | |
| cont.innerHTML = ''; | |
| // is_admin comes inside the /servers payload now (the old /admin probe | |
| // was a second Workers request on every list refresh). | |
| document.getElementById('adminBtn').style.display = d.is_admin ? 'inline-block' : 'none'; | |
| if(!(d.servers || []).length){ | |
| cont.innerHTML = `<div class="empty"><div class="big">🚀</div><b>No servers yet</b><br>Click <b>+ New Server</b> above to host your first script.</div>`; | |
| return; | |
| } | |
| (d.servers || []).forEach(s => { | |
| s.key = s.key || s.folder; | |
| const div = document.createElement('div'); | |
| div.className = 'card'; | |
| const cls = statusClass(s.status); | |
| const label = (s.status || 'Offline').toUpperCase(); | |
| // Prefer the DIRECT gateway link (bypasses the Pages edge entirely, so | |
| // site visits don't burn the dashboard account's quota). | |
| const siteHref = s.site_url || (s.subdomain ? `/web/${encodeURIComponent(s.subdomain)}/` : ''); | |
| const siteLink = siteHref | |
| ? `<a class="srv-site" href="${escapeAttr(siteHref)}" target="_blank" rel="noopener" onclick="event.stopPropagation()" title="Open site (direct)">🌐 /web/${escapeHtml(s.subdomain || '')}/</a>` | |
| : ''; | |
| div.innerHTML = ` | |
| <div class="srv-title">${escapeHtml(s.title)}</div> | |
| <div class="srv-sub">${escapeHtml(s.subtitle || '')}${s.type && s.type !== 'python' ? ' · ' + escapeHtml(s.type) : ''}</div> | |
| <div class="pill ${cls}"><span class="dot"></span>${label}</div> | |
| ${siteLink} | |
| <button class="btn btn-mini btn-danger card-del" title="Delete server">🗑</button> | |
| `; | |
| div.onclick = () => openServer(s); | |
| div.querySelector('.card-del').onclick = (e) => { | |
| e.stopPropagation(); | |
| confirmDeleteServer(s.key, s.title); | |
| }; | |
| cont.appendChild(div); | |
| }); | |
| } | |
| async function confirmDeleteServer(key, title) { | |
| if(!confirm(`Delete server "${title}" permanently?\nAll its files, logs and data will be removed.`)) return; | |
| if(!confirm(`Are you sure about deleting "${title}"? This cannot be undone.`)) return; | |
| const r = await fetch(`/server/delete/${encodeURIComponent(key)}`, {method:'POST'}); | |
| const d = await r.json().catch(()=>({success:false, message:'Error'})); | |
| if(!d.success){ alert(d.message || 'Failed to delete server'); return; } | |
| if(currentSrv && currentSrv.key === key){ | |
| currentSrv = null; | |
| goHome(); | |
| } else { | |
| loadServers(); | |
| } | |
| loadPythonHost(); | |
| } | |
| function deleteServer() { | |
| if(!currentSrv) return; | |
| confirmDeleteServer(currentSrv.key, currentSrv.title || currentSrv.folder); | |
| } | |
| function openServer(s) { | |
| currentSrv = s; | |
| currentPath = ''; | |
| currentSrv._site = s.site_url || ''; | |
| currentSrv._wh = {enabled: !!s.webhook, url: ''}; | |
| document.getElementById('homeView').style.display = 'none'; | |
| document.getElementById('serverView').style.display = 'block'; | |
| document.getElementById('homeBtn').style.display = 'inline-block'; | |
| document.getElementById('newBtn').style.display = 'none'; | |
| document.getElementById('srvHeader').textContent = s.title; | |
| document.getElementById('srvSub').textContent = s.subtitle || ''; | |
| document.getElementById('mainFileTxt').textContent = s.startup_file || "Click to set"; | |
| renderExtraRows(); | |
| switchTab('console'); | |
| } | |
| function goHome() { | |
| document.getElementById('homeView').style.display='block'; | |
| document.getElementById('serverView').style.display='none'; | |
| document.getElementById('homeBtn').style.display='none'; | |
| document.getElementById('newBtn').style.display='inline-block'; | |
| clearInterval(poll); | |
| poll = null; | |
| currentSrv = null; | |
| loadServers(); | |
| } | |
| function switchTab(t) { | |
| document.getElementById('tabConsole').classList.toggle('active', t==='console'); | |
| document.getElementById('tabFiles').classList.toggle('active', t==='files'); | |
| document.getElementById('viewConsole').style.display = t === 'console' ? 'block' : 'none'; | |
| document.getElementById('viewFiles').style.display = t === 'files' ? 'block' : 'none'; | |
| if(t === 'console') startPolling(); | |
| else { clearInterval(poll); loadFiles(); } | |
| } | |
| async function startPolling() { | |
| if(poll) clearInterval(poll); | |
| const fn = async () => { | |
| if(document.hidden) return; // hidden tab -> skip stats calls (save quota) | |
| try { | |
| const r = await fetch(`/server/stats/${encodeURIComponent(currentSrv.key)}`); | |
| if (r.redirected){ safeRedirect(r.url); return; } | |
| const d = await r.json(); | |
| document.getElementById('logOut').textContent = d.logs || "No logs"; | |
| document.getElementById('statusTxt').textContent = d.status || 'Offline'; | |
| document.getElementById('memTxt').textContent = d.mem || '0 MB'; | |
| document.getElementById('ipTxt').textContent = d.ip || ''; | |
| const running = (d.status === "Running" || d.status === "Installing" || d.status === "Starting"); | |
| document.getElementById('btnStart').disabled = running; | |
| currentSrv._site = d.site_url || ''; | |
| if(d.webhook) currentSrv._wh = d.webhook; | |
| renderExtraRows(); | |
| const lo = document.getElementById('logOut'); | |
| lo.scrollTop = lo.scrollHeight; | |
| } catch(e) {} | |
| }; | |
| fn(); | |
| poll = setInterval(fn, 5000); | |
| } | |
| // Catch up instantly when the tab becomes visible again (paused while hidden). | |
| document.addEventListener('visibilitychange', () => { | |
| if(document.hidden || !currentSrv) return; | |
| if(document.getElementById('serverView').style.display === 'none') return; | |
| const vc = document.getElementById('viewConsole'); | |
| if(vc && vc.style.display !== 'none') startPolling(); | |
| }); | |
| function renderExtraRows(){ | |
| if(!currentSrv) return; | |
| const isSite = !!currentSrv.subdomain && ['webapp','api','php'].includes(currentSrv.type); | |
| const siteRow = document.getElementById('siteRow'); | |
| siteRow.style.display = isSite && currentSrv._site ? '' : 'none'; | |
| if(isSite && currentSrv._site){ | |
| document.getElementById('siteTxt').textContent = currentSrv._site.replace(/^https?:\/\//,'').replace(/\/$/,''); | |
| } | |
| const whRow = document.getElementById('webhookRow'); | |
| whRow.style.display = isSite ? '' : 'none'; | |
| if(isSite){ | |
| const wh = currentSrv._wh || {enabled:false, url:''}; | |
| const txt = document.getElementById('webhookTxt'); | |
| txt.textContent = wh.enabled ? 'ON \u00B7 tap to disable' : 'OFF \u00B7 tap to enable'; | |
| txt.style.color = wh.enabled ? '#5CE1A4' : ''; | |
| whRow.title = wh.enabled ? (wh.url || '') : 'Telegram delivers updates to this bot\u2019s public URL (no polling)'; | |
| } | |
| } | |
| function openSite(){ | |
| if(currentSrv && currentSrv._site) window.open(currentSrv._site, '_blank', 'noopener'); | |
| } | |
| async function toggleWebhook(){ | |
| if(!currentSrv) return; | |
| const wh = currentSrv._wh || {enabled:false, url:''}; | |
| const enable = !wh.enabled; | |
| if(enable){ | |
| if(!confirm('Turn ON Telegram webhook mode for this bot?\n\n- The panel calls setWebhook now, and again automatically on every start.\n- Stop/delete removes it automatically.\n- The bot must NOT use getUpdates polling (Telegram refuses both at once).')) return; | |
| } | |
| const r = await fetch(`/server/set-webhook/${encodeURIComponent(currentSrv.key)}`, { | |
| method: 'POST', | |
| headers: {'Content-Type':'application/json'}, | |
| body: JSON.stringify({enabled: enable}) | |
| }); | |
| const d = await r.json().catch(()=>({success:false, message:'Error'})); | |
| if(!d.success){ toast(d.message || 'Webhook change failed'); return; } | |
| currentSrv._wh = {enabled: !!d.enabled, url: d.url || ''}; | |
| renderExtraRows(); | |
| toast(d.enabled ? 'Telegram webhook enabled' : 'Telegram webhook disabled'); | |
| } | |
| async function srvAction(act) { | |
| await fetch(`/server/action/${encodeURIComponent(currentSrv.key)}/${act}`, {method:'POST'}); | |
| } | |
| function changeStartup() { | |
| openM("Set Main File (example: bot.py)", currentSrv.startup_file || "", async (v) => { | |
| await fetch(`/server/set-startup/${encodeURIComponent(currentSrv.key)}`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({file:v}) | |
| }); | |
| currentSrv.startup_file = v; | |
| document.getElementById('mainFileTxt').textContent = v || "Click to set"; | |
| closeM(); | |
| }); | |
| } | |
| async function loadFiles() { | |
| const r = await fetch(`/files/list/${encodeURIComponent(currentSrv.key)}?path=${encodeURIComponent(currentPath)}`); | |
| if (r.redirected){ safeRedirect(r.url); return; } | |
| const data = await r.json(); | |
| if (!data.success) { currentPath = ''; return loadFiles(); } | |
| document.getElementById("btnBack").style.display = (currentPath && currentPath.length > 0) ? "inline-block" : "none"; | |
| const parts = (data.path || '').split('/').filter(Boolean); | |
| let acc = ''; | |
| const crumbs = [`<span class="crumb"><a href="javascript:void(0)" onclick="jumpPath('')">root</a></span>`]; | |
| for (const p of parts) { | |
| acc = acc ? (acc + '/' + p) : p; | |
| crumbs.push(`<span class="sep">/</span><span class="crumb"><a href="javascript:void(0)" onclick="jumpPath('${escapeAttr(acc)}')">${escapeHtml(p)}</a></span>`); | |
| } | |
| document.getElementById('pathBar').innerHTML = crumbs.join(''); | |
| const rows = []; | |
| for (const d of (data.dirs || [])) { | |
| const name = d.name; | |
| rows.push(` | |
| <tr> | |
| <td><span class="name-link" onclick="openFolder('${escapeAttr(name)}')">📁 ${escapeHtml(name)}</span></td> | |
| <td style="color:var(--muted)">—</td> | |
| <td class="row-actions"> | |
| <button class="btn btn-mini" onclick="openFolder('${escapeAttr(name)}')">Open</button> | |
| <button class="btn btn-mini" onclick="renameDir('${escapeAttr(name)}')">Rename</button> | |
| <button class="btn btn-mini btn-danger" onclick="deleteDir('${escapeAttr(name)}')">Delete</button> | |
| </td> | |
| </tr> | |
| `); | |
| } | |
| for (const f of (data.files || [])) { | |
| const relFile = currentPath ? `${currentPath}/${f.name}` : f.name; | |
| rows.push(` | |
| <tr> | |
| <td><span class="name-link" onclick="openEditor('${escapeAttr(relFile)}')">📄 ${escapeHtml(f.name)}</span></td> | |
| <td>${escapeHtml(f.size || '')}</td> | |
| <td class="row-actions"> | |
| <button class="btn btn-mini" onclick="renameFilePrompt('${escapeAttr(f.name)}')">Rename</button> | |
| <button class="btn btn-mini btn-danger" onclick="deleteFilePrompt('${escapeAttr(f.name)}')">Delete</button> | |
| </td> | |
| </tr> | |
| `); | |
| } | |
| document.getElementById('fileList').innerHTML = rows.join(''); | |
| } | |
| function openFolder(name) { | |
| currentPath = currentPath ? (currentPath + '/' + name) : name; | |
| loadFiles(); | |
| } | |
| function jumpPath(p) { currentPath = p || ''; loadFiles(); } | |
| function goUp() { | |
| if (!currentPath) return; | |
| const parts = currentPath.split('/').filter(Boolean); | |
| parts.pop(); | |
| currentPath = parts.join('/'); | |
| loadFiles(); | |
| } | |
| function newFolder() { | |
| openM("Create Folder", "", async (v) => { | |
| await fetch(`/files/mkdir/${encodeURIComponent(currentSrv.key)}`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({path: currentPath, name: v}) | |
| }); | |
| loadFiles(); | |
| closeM(); | |
| }); | |
| } | |
| function renameDir(oldName) { | |
| openM("Rename Folder", oldName, async (v) => { | |
| await fetch(`/files/rename/${encodeURIComponent(currentSrv.key)}`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({path: currentPath, old: oldName, new: v}) | |
| }); | |
| loadFiles(); | |
| closeM(); | |
| }); | |
| } | |
| async function deleteDir(name) { | |
| if(!confirm(`Delete folder "${name}" and all inside?`)) return; | |
| await fetch(`/files/delete/${encodeURIComponent(currentSrv.key)}`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({path: currentPath, name, kind:'dir'}) | |
| }); | |
| loadFiles(); | |
| } | |
| function renameFilePrompt(oldName) { | |
| openM("Rename File", oldName, async (v) => { | |
| await fetch(`/files/rename/${encodeURIComponent(currentSrv.key)}`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({path: currentPath, old: oldName, new: v}) | |
| }); | |
| loadFiles(); | |
| closeM(); | |
| }); | |
| } | |
| async function deleteFilePrompt(name) { | |
| if(!confirm(`Delete ${name}?`)) return; | |
| await fetch(`/files/delete/${encodeURIComponent(currentSrv.key)}`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({path: currentPath, name, kind:'file'}) | |
| }); | |
| loadFiles(); | |
| } | |
| async function doUploadFiles(input) { | |
| if (!input.files || !input.files.length) return; | |
| const fd = new FormData(); | |
| for (const f of input.files) fd.append("files", f); | |
| await fetch(`/files/upload/${encodeURIComponent(currentSrv.key)}?path=${encodeURIComponent(currentPath)}`, { | |
| method:"POST", | |
| body: fd | |
| }); | |
| input.value = ""; | |
| loadFiles(); | |
| } | |
| async function doUploadFolder(input) { | |
| if (!input.files || !input.files.length) return; | |
| const fd = new FormData(); | |
| for (const f of input.files) { | |
| fd.append("files", f); | |
| fd.append("relpaths", f.webkitRelativePath || f.name); | |
| } | |
| await fetch(`/files/upload/${encodeURIComponent(currentSrv.key)}?path=${encodeURIComponent(currentPath)}`, { | |
| method:"POST", | |
| body: fd | |
| }); | |
| input.value = ""; | |
| loadFiles(); | |
| } | |
| async function openEditor(name) { | |
| currentFile = name; | |
| document.getElementById('ed-title').textContent = name; | |
| const res = await fetch(`/files/content/${encodeURIComponent(currentSrv.key)}?file=${encodeURIComponent(name)}`); | |
| if (res.redirected){ safeRedirect(res.url); return; } | |
| const data = await res.json(); | |
| document.getElementById('ed-area').value = data.content || ''; | |
| document.getElementById('editor-modal').style.display = 'block'; | |
| } | |
| async function saveFile() { | |
| const r = await fetch(`/files/save/${encodeURIComponent(currentSrv.key)}`, { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({file: currentFile, content: document.getElementById('ed-area').value}) | |
| }); | |
| toast(r.ok ? 'Saved ✓' : 'Save failed'); | |
| } | |
| function closeEditor(){ document.getElementById('editor-modal').style.display='none'; } | |
| function openM(t, v, cb) { | |
| document.getElementById('modalBackdrop').style.display='flex'; | |
| document.getElementById('mTitle').textContent=t; | |
| document.getElementById('mIn').value=v || ""; | |
| document.getElementById('mConfirm').onclick=()=>cb(document.getElementById('mIn').value); | |
| } | |
| function closeM(){ document.getElementById('modalBackdrop').style.display='none'; } | |
| document.getElementById('newBtn').onclick = () => { | |
| document.getElementById('nsName').value=''; document.getElementById('nsSub').value=''; | |
| document.getElementById('newServerBackdrop').style.display='flex'; | |
| }; | |
| document.getElementById('nsCreate').onclick = async () => { | |
| const v = document.getElementById('nsName').value.trim(); | |
| const type = document.getElementById('nsType').value; | |
| const subdomain = document.getElementById('nsSub').value.trim(); | |
| if(!v){ toast('Enter a name'); return; } | |
| const res = await fetch('/add', { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({name:v, type, subdomain}) | |
| }); | |
| const d = await res.json().catch(()=>({success:false,message:'Error'})); | |
| if(!d.success){ toast(d.message || 'Failed'); return; } | |
| document.getElementById('newServerBackdrop').style.display='none'; | |
| toast(type==='php'||type==='webapp'||type==='api' ? 'Created — upload your files and Start' : 'Created'); | |
| loadServers(); | |
| loadPythonHost(); | |
| }; | |
| function toast(msg){ | |
| const t=document.getElementById('toast'); | |
| t.textContent=msg||'Done'; | |
| t.classList.add('show'); | |
| clearTimeout(t._h); | |
| t._h=setTimeout(()=>t.classList.remove('show'),2200); | |
| } | |
| function copyLogs(){ | |
| const txt=document.getElementById('logOut').textContent||''; | |
| const done=()=>toast('Console output copied'); | |
| if(navigator.clipboard && navigator.clipboard.writeText){ | |
| navigator.clipboard.writeText(txt).then(done).catch(()=>toast('Copy failed')); | |
| }else{ | |
| const ta=document.createElement('textarea'); | |
| ta.value=txt; document.body.appendChild(ta); ta.select(); | |
| try{ document.execCommand('copy'); done(); }catch(e){ toast('Copy failed'); } | |
| document.body.removeChild(ta); | |
| } | |
| } | |
| function escapeHtml(s){ | |
| return (s||'') | |
| .replaceAll('&','&') | |
| .replaceAll('<','<') | |
| .replaceAll('>','>') | |
| .replaceAll('"','"') | |
| .replaceAll("'","'"); | |
| } | |
| function escapeAttr(s){ return escapeHtml(s).replaceAll("\\","\\\\"); } | |
| document.addEventListener('DOMContentLoaded', ()=>{ | |
| loadServers(); | |
| loadPythonHost(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |