Phase 1: ProjectEngine + PyEnvManager + project management WebUI

- New: services/project_engine.py (async subprocess manager)
- New: services/pyenv_manager.py (Python version + venv + git clone)
- New: services/web_panel/routes/projects.py (5 REST endpoints)
- New: static/web_panel/pages/projects.html (WebUI)
- Tests: 28/28 passing, API verified (GET /api/projects)
- Docs: Phase1_Progress.md (local)
This commit is contained in:
2026-06-10 20:41:24 +08:00
parent 7e762b68fe
commit 8dbf628dd6
11 changed files with 1012 additions and 35 deletions
+119
View File
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>项目管理 - SenSu</title>
<style>
body{font-family:monospace;background:#0e1416;color:#e0e3e4;margin:0;padding:16px}
h2{color:#00bcd4;border-bottom:1px solid #333;padding-bottom:8px}
.card{background:#1a1f21;border-radius:8px;padding:12px;margin:8px 0}
input,button,select{padding:6px 10px;margin:4px;border-radius:4px;border:1px solid #444;background:#222;color:#e0e3e4}
button{background:#00bcd4;color:#000;cursor:pointer;font-weight:bold}
button:hover{opacity:0.8}
button.danger{background:#ff5252}
.status-running{color:#4caf50}.status-stopped{color:#ff5252}.status-error{color:#ff9800}
.logs{background:#000;color:#0f0;padding:8px;border-radius:4px;max-height:300px;overflow-y:auto;font-size:12px}
.tabs{display:flex;gap:4px;margin-bottom:12px}
.tab{padding:6px 12px;background:#333;border-radius:4px 4px 0 0;cursor:pointer}
.tab.active{background:#00bcd4;color:#000}
</style></head>
<body>
<h2>📦 项目管理</h2>
<div class="tabs">
<div class="tab active" onclick="showTab('running')">运行中</div>
<div class="tab" onclick="showTab('add')">+ 添加项目</div>
</div>
<div id="tab-running">
<div id="project-list">加载中...</div>
</div>
<div id="tab-add" style="display:none">
<div class="card">
<h3>添加新项目</h3>
<input id="proj-name" placeholder="项目名称" style="width:200px"><br>
<input id="proj-cmd" placeholder="启动命令 (如: python3 main.py)" style="width:400px"><br>
<input id="proj-cwd" placeholder="工作目录 (默认: .)" value="." style="width:200px"><br>
<input id="proj-port" placeholder="端口 (可选)" type="number" style="width:100px"><br>
<input id="proj-proxy" placeholder="代理路径 (可选, 如: /myapp)" style="width:200px"><br>
<button onclick="addProject()">启动项目</button>
</div>
<div class="card">
<h3>从 Git 部署</h3>
<input id="git-url" placeholder="Git URL" style="width:400px"><br>
<input id="git-branch" placeholder="分支 (默认: main)" value="main" style="width:200px"><br>
<button onclick="deployGit()">克隆并部署</button>
</div>
</div>
<script>
let projects=[];
async function refresh(){
try{
const r=await fetch("/SenSu/api/projects");
const d=await r.json();
projects=d.projects;
let h="";
for(const p of projects){
const cls=p.status==="running"?"status-running":p.status==="error"?"status-error":"status-stopped";
h+=`<div class="card">
<b>${p.name}</b> <span class="${cls}">● ${p.status}</span>
${p.pid?` PID:${p.pid}`:""} ${p.port?` :${p.port}`:""} ${p.uptime?` ${p.uptime}s`:""}
${p.proxy?`${p.proxy}`:""}
<button class="danger" onclick="stopProject('${p.name}')">停止</button>
<button onclick="viewLogs('${p.name}')">日志</button>
<button onclick="sendCmd('${p.name}')">发送命令</button>
<div id="logs-${p.name}" class="logs" style="display:none"></div>
</div>`;
}
document.getElementById("project-list").innerHTML=h||"暂无项目";
}catch(e){console.error(e)}
}
async function addProject(){
const name=document.getElementById("proj-name").value;
const cmdStr=document.getElementById("proj-cmd").value;
if(!name||!cmdStr){alert("请填写名称和命令");return}
await fetch("/SenSu/api/projects/run",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({
name,cwd:document.getElementById("proj-cwd").value,
cmd:cmdStr.split(" "),
port:parseInt(document.getElementById("proj-port").value)||0,
proxy_path:document.getElementById("proj-proxy").value
})
});
refresh();
}
async function deployGit(){
const url=document.getElementById("git-url").value;
const branch=document.getElementById("git-branch").value;
if(!url){alert("请填写 Git URL");return}
const name=url.split("/").pop().replace(".git","");
const resp=await fetch("/SenSu/api/projects/run",{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({name,cmd:["git","clone","-b",branch,url,name],cwd:"data/projects"})
});
refresh();
}
async function stopProject(name){
await fetch(`/SenSu/api/projects/${name}/stop`,{method:"POST"});
refresh();
}
async function viewLogs(name){
const el=document.getElementById("logs-"+name);
el.style.display=el.style.display==="none"?"block":"none";
if(el.style.display==="block"){
const r=await fetch(`/SenSu/api/projects/${name}/logs?tail=30`);
const d=await r.json();
el.innerHTML=d.logs.join("\n")||"(无日志)";
}
}
async function sendCmd(name){
const text=prompt("发送命令到 "+name+":");
if(text) await fetch(`/SenSu/api/projects/${name}/stdin`,{
method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({text})
});
}
function showTab(t){document.querySelectorAll("#tab-running,#tab-add").forEach(e=>e.style.display="none");document.getElementById("tab-"+t).style.display="block"}
setInterval(refresh,3000);refresh();
</script>
</body></html>