fix(tunnel): close stdin so autossh survives backgrounding; add quick "+ Тоннель" button

Background autossh died immediately because nohup left stdin attached to the
closed curl pipe — autossh saw EOF and bailed. Add </dev/null and wrap in
subshell so the process is fully detached from the install script's session.

Also add an "+ Тоннель" button next to "+ Добавить": creates a router with a
placeholder base URL and opens the tunnel modal in one click — for routers
without a public IP that have nothing to put in the base URL field yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Андрей Бобырев
2026-04-27 22:30:43 +03:00
parent 61d80ebc22
commit 3f2d621d02
2 changed files with 40 additions and 6 deletions

View File

@@ -127,6 +127,7 @@
<div style="min-width:100px"><label>Логин (если не .env)</label><input type="text" id="rl" placeholder="пусто = из .env"/></div>
<div style="min-width:120px"><label>Пароль (если не .env)</label><input type="password" id="rp" placeholder="пусто = из .env" autocomplete="new-password"/></div>
<div><label>&nbsp;</label><button class="btn btn-b" onclick="addR()">+ Добавить</button></div>
<div><label>&nbsp;</label><button class="btn" style="background:#7c3aed;color:#fff" onclick="addAndTunnel()" title="Создать роутер с base URL=http://localhost:NNNN и сразу открыть модалку тоннеля. Удобно для роутеров без белого IP.">⇄ Тоннель</button></div>
</div>
<div style="overflow-x:auto">
<table class="tbl" id="rtbl"><thead><tr>
@@ -359,6 +360,36 @@ async function addR(){
document.getElementById('rl').value='';document.getElementById('rp').value='';
await load();
}
async function addAndTunnel(){
// Создать роутер с placeholder base URL (заменится после "Применить URL автоматически")
// и сразу открыть модалку тоннеля.
let name=document.getElementById('rn').value.trim();
let keenetic_login=document.getElementById('rl').value.trim();
let keenetic_password=document.getElementById('rp').value;
if(!name){
name=prompt('Имя роутера (для тоннеля):','Роутер без IP');
if(!name)return;
}
if(!keenetic_login){keenetic_login='admin';}
if(!keenetic_password){
const p=prompt('Пароль Keenetic для RCI (поле «Пароль» пустое):');
if(p===null)return;
keenetic_password=p;
}
await saveServer({skipLoad:true});
// base URL — временный, реальный придёт из tunnel-cmd (http://localhost:NNNN)
const r=await fetch('/api/routers',{method:'POST',headers:hdr(),body:JSON.stringify({
name, rci_base_url:'http://localhost:1', keenetic_login, keenetic_password
})});
if(r.status===401){logout();return;}
if(!r.ok){alert(await r.text());return;}
const j=await r.json();
document.getElementById('rn').value='';document.getElementById('ru').value='';
document.getElementById('rl').value='';document.getElementById('rp').value='';
await load();
// Открыть модалку тоннеля для нового роутера
await tunnelR(j.id);
}
async function delR(id){
if(!confirm('Удалить?'))return;
const r=await fetch('/api/routers/'+encodeURIComponent(id),{method:'DELETE',headers:hdr()});