feat: 维护页 Tailscale 管理(设备列表/改名/授权/密钥不过期/设置IP/auth key,独立模块 core/tailscale_client.py)+ 密钥迁移 .env(STF_TOKEN/WEB_SECRET_KEY/TAILSCALE_API_KEY 不再入库)+ 修复设备更新端点(/device/{id} 405,改用 /name /authorized /key 专属端点)+ auth key description 仅 ASCII
This commit is contained in:
+117
-1
@@ -289,7 +289,7 @@ function showTab(name){
|
||||
if(name==='monitor'){loadMonitor();_monitorTimer=setInterval(loadMonitor,5000);}
|
||||
if(name==='tasks'){loadTasks();loadCustomActions();}
|
||||
if(name==='apps')loadApks();
|
||||
if(name==='maintenance')loadAdbDevices();
|
||||
if(name==='maintenance'){loadAdbDevices();loadTailscaleDevices();}
|
||||
if(name==='groups')loadGroups();
|
||||
if(name==='logs'){loadLogs();if(document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);}
|
||||
if(name==='users')loadUsers();
|
||||
@@ -2267,6 +2267,122 @@ function closeTestModal(){
|
||||
if(ov)ov.classList.remove('show');
|
||||
}
|
||||
|
||||
// ================== Tab: 维护 - Tailscale 管理 ==================
|
||||
async function loadTailscaleDevices(){
|
||||
const tb=document.getElementById('tb-tailscale');
|
||||
const st=document.getElementById('ts-status');
|
||||
const hint=document.getElementById('ts-hint');
|
||||
if(!tb)return;
|
||||
tb.innerHTML='<tr><td colspan="8" class="empty">加载中...</td></tr>';
|
||||
const r=await apiGet('/api/tailscale/devices');
|
||||
if(!r)return;
|
||||
if(!r.ok){
|
||||
tb.innerHTML='<tr><td colspan="8" class="empty">'+esc(r.error||'获取失败')+'</td></tr>';
|
||||
if(st)st.textContent='';
|
||||
const s=await apiGet('/api/tailscale/status');
|
||||
if(s&&s.ok&&!s.configured&&hint)hint.textContent='⚠ 未配置:'+esc(s.hint||'')+'(写入 .env 后重启服务生效)';
|
||||
return;
|
||||
}
|
||||
if(hint)hint.textContent='';
|
||||
const devs=r.devices||[];
|
||||
if(!devs.length){tb.innerHTML='<tr><td colspan="8" class="empty">tailnet 暂无设备</td></tr>';if(st)st.textContent='';return;}
|
||||
tb.innerHTML=devs.map(d=>{
|
||||
const ip=(d.addresses||[])[0]||'-';
|
||||
return '<tr>'+
|
||||
'<td><code>'+esc(d.hostname||'-')+'</code></td>'+
|
||||
'<td>'+esc(d.name||'-')+'</td>'+
|
||||
'<td style="font-family:monospace">'+esc(ip)+'</td>'+
|
||||
'<td>'+esc(d.os||'-')+'</td>'+
|
||||
'<td>'+((d.online===true)?'<span class="label label-success">在线</span>':(d.online===false?'<span class="label label-default">离线</span>':'<span class="label label-warning" title="12 小时内见过但当前未上报">最近在线</span>'))+'</td>'+
|
||||
'<td>'+(d.authorized?'<span class="label label-success">已授权</span>':'<span class="label label-danger">未授权</span>')+'</td>'+
|
||||
'<td>'+(d.key_expiry_disabled?'<span class="label label-warning">永不过期</span>':'<span class="label label-default">会过期</span>')+'</td>'+
|
||||
'<td style="white-space:nowrap">'+
|
||||
'<button class="btn btn-xs" onclick="renameTailscaleDevice(\''+esc(d.id)+'\',\''+esc(d.name||'')+'\')">改名</button> '+
|
||||
'<button class="btn btn-xs" onclick="setTsIp(\''+esc(d.id)+'\',\''+esc((d.addresses||[])[0]||'')+'\')" title="修改设备的 Tailscale IPv4(会断开当前连接,平台 serial 随之变化)">设置IP</button> '+
|
||||
'<button class="btn btn-xs" onclick="toggleTsAuth(\''+esc(d.id)+'\','+(!d.authorized)+')">'+(d.authorized?'取消授权':'授权')+'</button> '+
|
||||
'<button class="btn btn-xs" onclick="toggleTsKeyExpiry(\''+esc(d.id)+'\','+(!d.key_expiry_disabled)+')">'+(d.key_expiry_disabled?'恢复过期':'密钥不过期')+'</button> '+
|
||||
'<button class="btn btn-danger btn-xs" onclick="deleteTailscaleDevice(\''+esc(d.id)+'\')">移除</button>'+
|
||||
'</td></tr>';
|
||||
}).join('');
|
||||
if(st)st.textContent='共 '+devs.length+' 台设备';
|
||||
}
|
||||
function renameTailscaleDevice(id, current){
|
||||
const name=prompt('修改显示名(只改名称,不影响主机名):', current||'');
|
||||
if(name===null)return;
|
||||
apiPost('/api/tailscale/devices/'+id,{name:name}).then(r=>{
|
||||
if(!r)return;
|
||||
if(r.ok){showToast(r.msg,'success');loadTailscaleDevices();}
|
||||
else showToast(r.error,'error');
|
||||
});
|
||||
}
|
||||
function setTsIp(id, current){
|
||||
const ip=prompt('设置新的 Tailscale IPv4 地址(如 100.100.10.16):\n\n⚠ 改 IP 会断开设备当前 tailscale 连接,且平台设备池(STF serial)随之变化,改完记得同步 STF 设备池/分组/任务目标。', (current||'').split(':')[0]||'');
|
||||
if(ip===null)return;
|
||||
const v=ip.trim();
|
||||
if(!v){showToast('IP 不能为空','error');return;}
|
||||
apiPost('/api/tailscale/devices/'+id+'/ip',{ipv4:v}).then(r=>{
|
||||
if(!r)return;
|
||||
if(r.ok){showToast(r.msg,'success');loadTailscaleDevices();}
|
||||
else showToast(r.error,'error');
|
||||
});
|
||||
}
|
||||
function toggleTsAuth(id, authorized){
|
||||
apiPost('/api/tailscale/devices/'+id,{authorized:authorized}).then(r=>{
|
||||
if(!r)return;
|
||||
if(r.ok){showToast(authorized?'已授权':'已取消授权','success');loadTailscaleDevices();}
|
||||
else showToast(r.error,'error');
|
||||
});
|
||||
}
|
||||
function toggleTsKeyExpiry(id, disabled){
|
||||
apiPost('/api/tailscale/devices/'+id,{key_expiry_disabled:disabled}).then(r=>{
|
||||
if(!r)return;
|
||||
if(r.ok){showToast(disabled?'已设置密钥永不过期(设备不再被踢下线)':'已恢复密钥定期过期','success');loadTailscaleDevices();}
|
||||
else showToast(r.error,'error');
|
||||
});
|
||||
}
|
||||
function deleteTailscaleDevice(id){
|
||||
if(!confirm('确定从 tailnet 移除该设备?\n设备下次上线需重新授权。'))return;
|
||||
apiDelete('/api/tailscale/devices/'+id).then(r=>{
|
||||
if(!r)return;
|
||||
if(r.ok){showToast(r.msg,'success');loadTailscaleDevices();}
|
||||
else showToast(r.error,'error');
|
||||
});
|
||||
}
|
||||
function openTailscaleAuthKey(){
|
||||
const desc=prompt('auth key 用途说明(可空,仅支持英文/数字):','auto_control');
|
||||
if(desc===null)return;
|
||||
const reusable=confirm('允许复用(同一 key 给多台设备用)?\n确定=可复用,取消=一次性');
|
||||
const secs=prompt('有效期(秒,默认 3600=1 小时):','3600');
|
||||
if(secs===null)return;
|
||||
const st=document.getElementById('ts-status');
|
||||
if(st)st.textContent='正在生成 auth key...';
|
||||
apiPost('/api/tailscale/authkey',{description:desc||'',reusable:reusable,expiry_seconds:parseInt(secs)||3600}).then(r=>{
|
||||
if(!r)return;
|
||||
if(st)st.textContent='';
|
||||
if(r.ok){
|
||||
const ov=document.getElementById('test-overlay');
|
||||
document.getElementById('test-modal-title').textContent='Tailscale Auth Key(只显示一次)';
|
||||
document.getElementById('test-modal-body').innerHTML=
|
||||
'<div style="margin-bottom:10px">新设备执行 <code>tailscale up --authkey=...</code> 接入(默认已预授权)</div>'+
|
||||
'<textarea id="ts-key-box" class="form-control" rows="4" style="font-family:monospace" readonly>'+esc(r.key||'')+'</textarea>'+
|
||||
'<button class="btn btn-primary" style="margin-top:10px" onclick="copyTsKey()">复制</button>'+
|
||||
' <span id="ts-key-status" class="text-muted"></span>';
|
||||
ov.classList.add('show');
|
||||
showToast('Auth Key 已生成','success');
|
||||
}else{
|
||||
showToast(r.error,'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
function copyTsKey(){
|
||||
const box=document.getElementById('ts-key-box');
|
||||
if(!box)return;
|
||||
box.select();
|
||||
try{document.execCommand('copy');}catch(e){}
|
||||
const s=document.getElementById('ts-key-status');
|
||||
if(s)s.textContent='已复制';
|
||||
}
|
||||
|
||||
// ================== Tab: 应用 ==================
|
||||
let _apksCache=[];
|
||||
let _installTimer=null;
|
||||
|
||||
Reference in New Issue
Block a user