feat(ai): F1-5b Wave A admin-web sandbox 透出 UI-1/2/4 (W1)

完成 F1-5b Wave A admin-web 改造:

UI-1 AIRunLogs 列表加 runtime_mode + sandbox_instance_id 列
- 后端 schema RunLogItem 补 runtime_mode / sandbox_instance_id 字段
- 后端 SQL list_run_logs SELECT 加这两列
- 前端 columns 加"运行模式"(orange/blue Tag) + "沙箱实例"(短哈希 + tooltip)

UI-2 AIRunLogs 详情 Drawer 加 runtime 字段
- 后端 SQL get_run_log SELECT 加 runtime 列
- 前端 Descriptions 加"运行模式" + "沙箱实例"两项

UI-4 全局 sandbox 徽章(覆盖所有 admin-web 页面)
- App.tsx Footer 三段式: 左 sandbox 徽章 / 中 任务状态 / 右 占位
- 30s 轮询 fetchRuntimeContext(userSiteId)
- sandbox: 橙色"沙箱"+ 业务日 + 短哈希实例 ID(monospace)
- live: 绿色"实时"+ 真实今天

双口径 4a/4b 验证(MCP Playwright 实地走查):
- UI-1 4a live: 列表全行 live 蓝 Tag
- UI-1 4b sandbox: SQL INSERT walkthrough_ui12 → 列表显示 sandbox 橙 Tag + 短哈希
- UI-2 4b: Drawer 详情 runtime_mode='sandbox' 橙 Tag + sandbox_instance_id monospace 全 ID
- UI-4 4a: footer 左侧绿"实时"+ 2026-05-05
- UI-4 4b: 切 sandbox=2026-04-20 后 footer 显示橙"沙箱"+ 业务日 + sbx_e7a7e5c5...
- 截图归档 docs/audit/changes/screenshots/2026-05-05_f1_5b_wave_a/

剩余 Wave A: MP-3/5 小程序 sandbox / MP-1 board-finance 字段复核 / BE-1 task-list 403

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Neo
2026-05-05 15:14:29 +08:00
parent af02446740
commit 3c8d72edd4
12 changed files with 90 additions and 4 deletions

View File

@@ -32,6 +32,8 @@ import { useAuthStore } from "./store/authStore";
import { useBusinessDayStore } from "./store/businessDayStore";
import { fetchQueue } from "./api/execution";
import type { QueuedTask } from "./types";
// F1-5b UI-4: 全局 sandbox 徽章
import { fetchRuntimeContext, type RuntimeContext } from "./api/runtimeContext";
import Login from "./pages/Login";
import EnvConfig from "./pages/EnvConfig";
import DBViewer from "./pages/DBViewer";
@@ -150,8 +152,11 @@ const AppLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const logout = useAuthStore((s) => s.logout);
const userSiteId = useAuthStore((s) => s.user?.site_id);
const [runningTask, setRunningTask] = useState<QueuedTask | null>(null);
// F1-5b UI-4: 当前 site 的 runtime context(sandbox 徽章渲染依据)
const [runtimeCtx, setRuntimeCtx] = useState<RuntimeContext | null>(null);
const pollQueue = useCallback(async () => {
try {
@@ -163,12 +168,29 @@ const AppLayout: React.FC = () => {
}
}, []);
// F1-5b UI-4: 拉 runtime context(30 秒轮询,sandbox 切换不需即时但 5 分钟内必须更新)
const pollRuntimeCtx = useCallback(async () => {
if (!userSiteId) return;
try {
const ctx = await fetchRuntimeContext(userSiteId);
setRuntimeCtx(ctx);
} catch {
// 失败不阻塞顶栏渲染
}
}, [userSiteId]);
useEffect(() => {
pollQueue();
const timer = setInterval(pollQueue, 5_000);
return () => clearInterval(timer);
}, [pollQueue]);
useEffect(() => {
pollRuntimeCtx();
const timer = setInterval(pollRuntimeCtx, 30_000);
return () => clearInterval(timer);
}, [pollRuntimeCtx]);
const onMenuClick: MenuProps["onClick"] = ({ key }) => { navigate(key); };
const handleLogout = () => {
@@ -266,12 +288,37 @@ const AppLayout: React.FC = () => {
</Content>
<Footer
style={{
textAlign: "center",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "6px 16px",
background: "#fafafa",
borderTop: "1px solid #f0f0f0",
}}
>
{/* F1-5b UI-4: 左侧 sandbox 徽章 - sandbox 模式始终可见,提醒离开 AIOperations 后仍知道在虚拟时间 */}
<div style={{ minWidth: 180, textAlign: "left" }}>
{runtimeCtx?.is_sandbox ? (
<Space size={6}>
<Tag color="orange" style={{ margin: 0 }}></Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
{runtimeCtx.business_date}
</Text>
<Text type="secondary" style={{ fontSize: 11, fontFamily: "monospace" }}>
{runtimeCtx.sandbox_instance_id?.slice(0, 12)}
</Text>
</Space>
) : runtimeCtx ? (
<Space size={6}>
<Tag color="green" style={{ margin: 0 }}></Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
{runtimeCtx.business_date}
</Text>
</Space>
) : null}
</div>
<div style={{ flex: 1, textAlign: "center" }}>
{runningTask ? (
<Space size={8}>
<Spin size="small" />
@@ -285,6 +332,9 @@ const AppLayout: React.FC = () => {
) : (
<Text type="secondary" style={{ fontSize: 12 }}></Text>
)}
</div>
{/* 右侧占位,保持 Footer 三段式平衡 */}
<div style={{ minWidth: 180 }} />
</Footer>
</Layout>
</Layout>

View File

@@ -139,6 +139,9 @@ export interface RunLogItem {
status: string;
site_id: number;
created_at: string;
// F1-5b UI-1: sandbox 透出
runtime_mode?: string | null;
sandbox_instance_id?: string | null;
}
export interface RunLogDetailResponse extends RunLogItem {

View File

@@ -101,6 +101,21 @@ const AIRunLogs: React.FC = () => {
title: "状态", dataIndex: "status", key: "status", width: 110,
render: (v: string) => <Tag color={STATUS_COLOR[v] ?? "default"}>{v}</Tag>,
},
{
title: "运行模式", dataIndex: "runtime_mode", key: "runtime_mode", width: 100,
render: (v: string | null) => {
if (!v) return "—";
return <Tag color={v === "sandbox" ? "orange" : "blue"}>{v}</Tag>;
},
},
{
title: "沙箱实例", dataIndex: "sandbox_instance_id", key: "sandbox_instance_id", width: 130,
render: (v: string | null) => {
if (!v || v === "live") return "—";
const short = v.length > 12 ? v.slice(0, 8) + "…" : v;
return <span title={v} style={{ fontFamily: "monospace", fontSize: 12 }}>{short}</span>;
},
},
{ title: "时间", dataIndex: "created_at", key: "created_at", width: 170, render: fmtTime },
];
@@ -192,6 +207,20 @@ const AIRunLogs: React.FC = () => {
<Tag color={STATUS_COLOR[detail.status] ?? "default"}>{detail.status}</Tag>
</Descriptions.Item>
<Descriptions.Item label="Session ID">{detail.session_id ?? "—"}</Descriptions.Item>
<Descriptions.Item label="运行模式">
{detail.runtime_mode ? (
<Tag color={detail.runtime_mode === "sandbox" ? "orange" : "blue"}>
{detail.runtime_mode}
</Tag>
) : "—"}
</Descriptions.Item>
<Descriptions.Item label="沙箱实例">
{detail.sandbox_instance_id && detail.sandbox_instance_id !== "live" ? (
<span style={{ fontFamily: "monospace", fontSize: 12 }}>
{detail.sandbox_instance_id}
</span>
) : "—"}
</Descriptions.Item>
<Descriptions.Item label="创建时间" span={2}>{fmtTime(detail.created_at)}</Descriptions.Item>
<Descriptions.Item label="完成时间" span={2}>{fmtTime(detail.finished_at)}</Descriptions.Item>
</Descriptions>