console: 项目导航移到左侧栏,工作台不再承担项目模块

左侧栏 ProjectNav:项目完整列表(名 + 短 id + 我的角色,
选中高亮,点击切换)+ 创建项目(名/GitHub 仓库/repo_url)
+ 选中项目成员(邀请/改角色/新增移除);工作台删除
sec-projects 一节与任务列表里的项目下拉框,侧栏为唯一
项目选择器,自动选第一个项目逻辑不变。
薄模块补齐(均为已核实后端路由):成员移除 DELETE、
自动化详情(GET 单条 + 产出)与编辑 PATCH、分诊条目点进
任务/自动化详情、Workers 搜索与在线筛选、管理区设置配额
POST。主线新功能(Todos、Browser、taskMeta 等)原样保留,
未做任何回退。console 标记单测本地 53 项通过。
This commit is contained in:
host committed 2026-10-30 07:43:54 +00:00
1 parent d6369154c2
commit 08484302ac
16 files changed
+293 -100

No files matched your search

@@ -2,7 +2,7 @@ import { useState } from "react";
import { useConsole } from "../../context";
import { Bar, DataList } from "../../components/ui";
import { fmt, shortId } from "../../api/client";
import type { Automation } from "../../api/types";
import type { Automation, AutomationDetail } from "../../api/types";
const PROMPT_PREVIEW = 160;
@@ -14,6 +14,49 @@ function humanInterval(s?: number): string {
return "";
}
// Automation detail (read-only display): backed by GET
// /projects/{pid}/automations/{aid} — full definition + runs[] +
// runs_note + heartbeat_binding. Editing is NOT duplicated here:
// the single edit form is main's inline edit in the list below
// (updateAutomation with the (a, name, prompt, interval, repo)
// signature). Detail display + runs + task buttons are our side's
// addition; cross-view jumps use location.hash because the IDE
// shell switches views on hashchange (no sec-conv / sec-automations
// element to scroll to).
function AutomationDetailBox({ detail }: { detail: AutomationDetail }) {
const ctx = useConsole();
const openTask = (id?: string) => {
if (!id) return;
ctx.showDetail(id);
try { window.location.hash = "#view-tasks"; } catch { /* noop */ }
};
return (
<div className="card">
<h2>自动化详情:{detail.name}</h2>
<div className="muted">
状态 {detail.status}{detail.due ? "|到期" : ""} | 每 {detail.interval_seconds} 秒 | 下次 {fmt(detail.next_run_at)}
| 上次执行 {fmt(detail.last_run_at)} | 创建人 {detail.created_by ? shortId(detail.created_by) : ""} | 创建于 {fmt(detail.created_at)}
{detail.cancelled_at ? " | 已取消于 " + fmt(detail.cancelled_at) : ""}
{detail.heartbeat_binding ? " | 心跳绑定:已绑定" : ""}
</div>
<div className="muted" style={{ whiteSpace: "pre-wrap", marginTop: 8 }}>{detail.prompt || ""}</div>
{detail.repo_url ? <div className="muted">仓库 {detail.repo_url}</div> : null}
<Bar>
<button onClick={ctx.closeAutomationDetail}>关闭详情</button>
<span className="muted">编辑请在下面列表里点“编辑”。{ctx.autoDetailMsg}</span>
</Bar>
<div className="data-group"><b>最近产出({(detail.runs || []).length})</b>
<DataList empty="(无)" items={(detail.runs || []).map((r, i) => (
<span key={i} className="row-main">
{r.status}{r.created_at ? "|" + fmt(r.created_at) : ""}{r.task_id ? <> | 任务 <button onClick={() => openTask(r.task_id as string)}>{shortId(r.task_id)}</button></> : ""}{r.detail ? "|" + String(r.detail).slice(0, 120) : ""}
</span>
))} />
</div>
{detail.runs_note ? <div className="muted">{detail.runs_note}</div> : null}
</div>
);
}
export function AutomationPanel() {
const ctx = useConsole();
const [name, setName] = useState("");
@@ -53,6 +96,7 @@ export function AutomationPanel() {
<div key={a.id}>
<div className="row-main">
[{a.status}{a.due ? "|到期" : ""}{a.run_once ? "|一次性" : ""}] {a.name}|每 {a.interval_seconds} 秒{humanInterval(a.interval_seconds)}|上次 {fmt(a.last_run_at) || "未运行"}|下次 {fmt(a.next_run_at)}
<button onClick={() => void ctx.loadAutomationDetail(a.id)}>详情</button>
{a.status === "active" ? <> <button onClick={() => startEdit(a)}>编辑</button> <button onClick={() => ctx.cancelAutomation(a.id)}>取消</button></> : null}
</div>
{a.repo_url ? <div className="muted">仓库:{a.repo_url}</div> : null}
@@ -80,6 +124,8 @@ export function AutomationPanel() {
</div>
);
})} />
{ctx.autoDetailMsg && !ctx.autoDetail ? <div className="muted">{ctx.autoDetailMsg}</div> : null}
{ctx.autoDetail ? <AutomationDetailBox key={ctx.autoDetail.id} detail={ctx.autoDetail} /> : null}
</>
);
}
@@ -87,21 +133,45 @@ export function AutomationPanel() {
export function TriagePanel() {
const ctx = useConsole();
const t = ctx.triage;
// Clicking an actionable item opens that task's detail (tasks view)
// or the automation's detail (automation view). Field names are
// exactly the triage payload's: failed_tasks[].id, pending_followups
// [].task_id, recent_runs[].task_id (nullable), automations_due[].id.
// Jumps use location.hash because the IDE shell switches views on
// hashchange; there is no sec-conv / sec-automations to scroll to.
const openTask = (id?: string) => {
if (!id) return;
ctx.showDetail(id);
try { window.location.hash = "#view-tasks"; } catch { /* noop */ }
};
const openAutomation = (id?: string) => {
if (!id) return;
void ctx.loadAutomationDetail(id);
try { window.location.hash = "#view-automation"; } catch { /* noop */ }
};
if (ctx.triageError) return <div className="muted">{ctx.triageError}</div>;
if (!t) return <div className="muted">(先选项目)</div>;
return (
<>
<div className="data-group"><b>到期自动化({t.counts?.automations_due || 0})</b>
<DataList empty="(无)" items={(t.automations_due || []).map((a, i) => <span key={i} className="row-main">{a.name}|每 {a.interval_seconds} 秒</span>)} />
<DataList empty="(无)" items={(t.automations_due || []).map((a, i) => (
<span key={i} className="row-main">{a.id ? <button onClick={() => openAutomation(a.id)}>{a.name}|每 {a.interval_seconds} 秒</button> : <>{a.name}|每 {a.interval_seconds} 秒</>}</span>
))} />
</div>
<div className="data-group"><b>失败/中断任务({t.counts?.failed_tasks || 0})</b>
<DataList empty="(无)" items={(t.failed_tasks || []).map((x, i) => <span key={i} className="row-main">{shortId(x.id)}|{x.status}|{String(x.prompt || "").slice(0, 80)}</span>)} />
<DataList empty="(无)" items={(t.failed_tasks || []).map((x, i) => (
<span key={i} className="row-main"><button onClick={() => openTask(x.id)}>{shortId(x.id)}|{x.status}|{String(x.prompt || "").slice(0, 80)}</button></span>
))} />
</div>
<div className="data-group"><b>待处理后续消息({t.counts?.pending_followups || 0})</b>
<DataList empty="(无)" items={(t.pending_followups || []).map((f, i) => <span key={i} className="row-main">任务 {shortId(f.task_id)}|{String(f.body || "").slice(0, 80)}</span>)} />
<DataList empty="(无)" items={(t.pending_followups || []).map((f, i) => (
<span key={i} className="row-main"><button onClick={() => openTask(f.task_id)}>任务 {shortId(f.task_id)}|{String(f.body || "").slice(0, 80)}</button></span>
))} />
</div>
<div className="data-group"><b>最近自动化产出({t.counts?.recent_runs || 0})</b>
<DataList empty="(无)" items={(t.recent_runs || []).map((r, i) => <span key={i} className="row-main">{shortId(r.automation_id)}|{r.status}{r.task_id ? "|任务 " + shortId(r.task_id) : ""}{r.task_status ? "|" + r.task_status : ""}</span>)} />
<DataList empty="(无)" items={(t.recent_runs || []).map((r, i) => (
<span key={i} className="row-main">{shortId(r.automation_id)}|{r.status}{r.task_id ? <>|任务 <button onClick={() => openTask(r.task_id)}>{shortId(r.task_id)}</button></> : ""}{r.task_status ? "|" + r.task_status : ""}</span>
))} />
</div>
</>
);