mirror of
https://github.com/andrey271192/domen_hydra.git
synced 2026-09-20 14:42:00 +00:00
feat: footer support links; POST /api/routers/{name}/push for single-router update
Made-with: Cursor
This commit is contained in:
@@ -68,6 +68,41 @@ async def _ssh_on_router(rcfg: dict, remote_cmd: str, timeout: int = 45) -> tupl
|
||||
except Exception as e:
|
||||
return 1, "", str(e)
|
||||
|
||||
|
||||
async def _push_one_router(server_url: str, router_key: str, rcfg: dict) -> dict:
|
||||
"""Скачать с manager domain.conf + ip.list на роутер по SSH и neo restart."""
|
||||
ip = rcfg.get("ip", "")
|
||||
if not ip:
|
||||
return {"router": router_key, "ok": False, "msg": "нет IP"}
|
||||
user = rcfg.get("user") or config.SSH_USER
|
||||
pwd = rcfg.get("password") or config.SSH_PASS
|
||||
cmd = (
|
||||
f"curl -sf '{server_url}/hydra/domain.conf' -o /opt/etc/HydraRoute/domain.conf && "
|
||||
f"curl -sf '{server_url}/hydra/ip.list' -o /opt/etc/HydraRoute/ip.list && "
|
||||
f"neo restart"
|
||||
)
|
||||
try:
|
||||
r = await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
[
|
||||
"sshpass", "-p", pwd, "ssh",
|
||||
"-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10",
|
||||
f"{user}@{ip}", cmd,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
tail = ((r.stdout or "") + (r.stderr or ""))[:400]
|
||||
return {
|
||||
"router": router_key,
|
||||
"ok": r.returncode == 0,
|
||||
"msg": tail or ("ok" if r.returncode == 0 else "exit " + str(r.returncode)),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"router": router_key, "ok": False, "msg": str(e)}
|
||||
|
||||
|
||||
# ── Pages ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
@@ -171,33 +206,7 @@ async def push_all(request: Request, x_admin_password: str = Header("")):
|
||||
server_url = str(request.base_url).rstrip("/")
|
||||
results = []
|
||||
for name, rcfg in routers.items():
|
||||
ip = rcfg.get("ip", "")
|
||||
if not ip:
|
||||
results.append({"router": name, "ok": False, "msg": "нет IP"})
|
||||
continue
|
||||
user = rcfg.get("user") or config.SSH_USER
|
||||
pwd = rcfg.get("password") or config.SSH_PASS
|
||||
cmd = (
|
||||
f"curl -sf '{server_url}/hydra/domain.conf' -o /opt/etc/HydraRoute/domain.conf && "
|
||||
f"curl -sf '{server_url}/hydra/ip.list' -o /opt/etc/HydraRoute/ip.list && "
|
||||
f"neo restart"
|
||||
)
|
||||
try:
|
||||
r = await asyncio.to_thread(
|
||||
subprocess.run,
|
||||
[
|
||||
"sshpass", "-p", pwd, "ssh",
|
||||
"-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=10",
|
||||
f"{user}@{ip}", cmd,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
tail = ((r.stdout or "") + (r.stderr or ""))[:400]
|
||||
results.append({"router": name, "ok": r.returncode == 0, "msg": tail or ("ok" if r.returncode == 0 else "exit " + str(r.returncode))})
|
||||
except Exception as e:
|
||||
results.append({"router": name, "ok": False, "msg": str(e)})
|
||||
results.append(await _push_one_router(server_url, name, rcfg))
|
||||
return {"results": results, "ok": sum(1 for r in results if r["ok"]), "failed": sum(1 for r in results if not r["ok"])}
|
||||
|
||||
# ── Routers CRUD ──────────────────────────────────────────────────────────────
|
||||
@@ -256,6 +265,17 @@ async def fetch_from_router(name: str, x_admin_password: str = Header("")):
|
||||
"errors": {"domain": err_d if code_d else "", "ip": err_i if code_i else ""},
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/routers/{name}/push")
|
||||
async def push_one_router(name: str, request: Request, x_admin_password: str = Header("")):
|
||||
"""Отправить текущий domain.conf + ip.list только на один роутер."""
|
||||
_chk(x_admin_password)
|
||||
key = _router_key(name)
|
||||
rcfg = _get_router_cfg(name)
|
||||
server_url = str(request.base_url).rstrip("/")
|
||||
return await _push_one_router(server_url, key, rcfg)
|
||||
|
||||
|
||||
@app.delete("/api/routers/{name}")
|
||||
async def delete_router(name: str, x_admin_password: str = Header("")):
|
||||
_chk(x_admin_password)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--bg:#000;--card:#1c1c1e;--card2:#2c2c2e;--card3:#3a3a3c;--border:#38383a;--text:#f5f5f7;--muted:#86868b;--accent:#0a84ff;--green:#30d158;--red:#ff453a;--yellow:#ffd60a;--orange:#ff9f0a}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display',sans-serif;background:var(--bg);color:var(--text);padding:20px 24px;max-width:1200px;margin:0 auto}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'SF Pro Display',sans-serif;background:var(--bg);color:var(--text);padding:20px 24px 56px;max-width:1200px;margin:0 auto}
|
||||
h1{font-size:22px;font-weight:700;margin-bottom:4px}
|
||||
.sub{color:var(--muted);font-size:13px;margin-bottom:20px}
|
||||
.topbar{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:12px;margin-bottom:20px;margin-top:12px}
|
||||
@@ -362,6 +362,7 @@ function _escHtml(s){ return String(s==null?'':s).replace(/&/g,'&').replace(
|
||||
const act=b.getAttribute('data-r-act');
|
||||
if(act==='test') await testRouter(n);
|
||||
else if(act==='pull') await pullRouter(n);
|
||||
else if(act==='push') await pushRouter(n);
|
||||
else if(act==='del') await delRouter(n);
|
||||
});
|
||||
})();
|
||||
@@ -376,6 +377,7 @@ async function loadRouters(){
|
||||
<div style="display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end">
|
||||
<button type="button" class="btn btn-ghost" style="font-size:11px;padding:5px 10px" data-r-act="test" data-router="${enc}">🔌 Тест</button>
|
||||
<button type="button" class="btn btn-b" style="font-size:11px;padding:5px 10px" data-r-act="pull" data-router="${enc}">⬇ С роутера</button>
|
||||
<button type="button" class="btn btn-g" style="font-size:11px;padding:5px 10px" data-r-act="push" data-router="${enc}">📡 На роутер</button>
|
||||
<button type="button" class="btn btn-r" style="font-size:11px;padding:5px 10px" data-r-act="del" data-router="${enc}">✗</button>
|
||||
</div></div>`;
|
||||
}).join('');
|
||||
@@ -406,10 +408,31 @@ async function pullRouter(n){
|
||||
sm('im','ok','✅ Подставлено: '+parts.join(', ')+(j.domain_ok&&j.ip_ok?'':' (часть файлов не найдена — пустое поле)'));
|
||||
}catch(e){ sm('rm','err','❌ '+e); }
|
||||
}
|
||||
async function pushRouter(n){
|
||||
const log=document.getElementById('router-ops-msg');
|
||||
log.style.display='block'; log.textContent='Отправка на «'+n+'»…\n';
|
||||
try{
|
||||
const r=await fetch('/api/routers/'+encodeURIComponent(n)+'/push',{method:'POST',headers:{'X-Admin-Password':getPass()}});
|
||||
const j=await r.json();
|
||||
if(!r.ok){ log.textContent='❌ '+(Array.isArray(j.detail)?j.detail.map(x=>x.msg||x).join(' '):(j.detail||r.status)); return; }
|
||||
log.textContent=(j.ok?'✅':'❌')+' «'+n+'»: '+(j.msg||'');
|
||||
}catch(e){ log.textContent='❌ '+e; }
|
||||
}
|
||||
|
||||
// ── SETTINGS ──────────────────────────────────────────────────────────────
|
||||
async function changePwd(){ const p1=document.getElementById('new-pwd').value; const p2=document.getElementById('new-pwd2').value; if(p1!==p2){sm('pwd-msg','err','❌ Пароли не совпадают');return;} if(p1.length<4){sm('pwd-msg','err','❌ Минимум 4 символа');return;} try{ const r=await fetch('/api/set_password',{method:'POST',headers:authHdr(),body:JSON.stringify({password:p1})}); if(r.ok){ sessionStorage.setItem('hm_pass',p1); sm('pwd-msg','ok','✅ Пароль сохранён'); document.getElementById('new-pwd').value=''; document.getElementById('new-pwd2').value=''; } }catch(e){sm('pwd-msg','err','❌ '+e)} }
|
||||
|
||||
function sm(id,c,t){const e=document.getElementById(id);e.className='msg '+c;e.textContent=t;e.style.display='block';setTimeout(()=>e.style.display='none',5000)}
|
||||
_checkAuth().then(ok=>{ if(ok) _afterLogin(); });
|
||||
</script></body></html>
|
||||
</script>
|
||||
<div id="hm-foot" lang="ru" style="position:fixed;bottom:10px;left:12px;z-index:90;max-width:min(96vw,720px);font-size:11px;font-weight:600;letter-spacing:.02em;color:#86868b;opacity:.92;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;line-height:1.3">
|
||||
<span style="color:#6e6e73;font-weight:500;margin-right:2px">поддержка:</span>
|
||||
<a href="https://github.com/andrey271192/domen_hydra" target="_blank" rel="noopener noreferrer" style="color:#a1a1a6;text-decoration:none;border-bottom:1px solid rgba(255,255,255,.12)">GitHub</a>
|
||||
<span style="color:#86868b;user-select:none">·</span>
|
||||
<a href="https://boosty.to/andrey27/donate" target="_blank" rel="noopener noreferrer" style="color:#a1a1a6;text-decoration:none;border-bottom:1px solid rgba(255,255,255,.12)">Boosty (донат)</a>
|
||||
<span style="color:#86868b;user-select:none">·</span>
|
||||
<a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/019dc200-2a5d-7931-a619-782d285f6798" target="_blank" rel="noopener noreferrer" title="Поддержка (Ozon Bank, СБП)" style="color:#a1a1a6;text-decoration:none;border-bottom:1px solid rgba(255,255,255,.12)">Ozon СБП</a>
|
||||
<span style="color:#86868b;user-select:none">·</span>
|
||||
<a href="https://t.me/Iot_andrey" target="_blank" rel="noopener noreferrer" style="color:#a1a1a6;text-decoration:none;border-bottom:1px solid rgba(255,255,255,.12)">Telegram @Iot_andrey</a>
|
||||
</div>
|
||||
</body></html>
|
||||
|
||||
Reference in New Issue
Block a user