Add dashboard docs and richer lab UI
这个提交包含在:
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `26`
|
- 待人工/缺浏览器证据: `26`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:57+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:57+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:10+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:56+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `14`
|
- 待人工/缺浏览器证据: `14`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `12`
|
- 待人工/缺浏览器证据: `12`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:58+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:12+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:57+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `1`
|
- 阻塞数: `1`
|
||||||
- 待人工/缺浏览器证据: `36`
|
- 待人工/缺浏览器证据: `36`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:15+00:00`
|
- 最近渲染时间: `2026-03-17T07:36:01+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:15+00:00`
|
- 最近渲染时间: `2026-03-17T07:36:01+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:15+00:00`
|
- 最近渲染时间: `2026-03-17T07:36:01+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:15+00:00`
|
- 最近渲染时间: `2026-03-17T07:36:01+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:15+00:00`
|
- 最近渲染时间: `2026-03-17T07:36:01+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:15+00:00`
|
- 最近渲染时间: `2026-03-17T07:36:01+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:15+00:00`
|
- 最近渲染时间: `2026-03-17T07:36:01+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- 已实证(synthetic): `0`
|
- 已实证(synthetic): `0`
|
||||||
- 阻塞数: `0`
|
- 阻塞数: `0`
|
||||||
- 待人工/缺浏览器证据: `0`
|
- 待人工/缺浏览器证据: `0`
|
||||||
- 最近渲染时间: `2026-03-17T07:27:13+00:00`
|
- 最近渲染时间: `2026-03-17T07:35:59+00:00`
|
||||||
|
|
||||||
## 目标约束
|
## 目标约束
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ function renderRunList() {
|
|||||||
const active = item.run_id === state.selectedRunId ? "is-active" : "";
|
const active = item.run_id === state.selectedRunId ? "is-active" : "";
|
||||||
const title = item.advisory_meta?.title || item.advisory_id;
|
const title = item.advisory_meta?.title || item.advisory_id;
|
||||||
const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || "";
|
const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || "";
|
||||||
|
const browserLabel = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "n/a");
|
||||||
return `
|
return `
|
||||||
<button class="run-card ${active}" data-run-id="${escapeHtml(item.run_id)}">
|
<button class="run-card ${active}" data-run-id="${escapeHtml(item.run_id)}">
|
||||||
<div class="run-card-top">
|
<div class="run-card-top">
|
||||||
@@ -180,7 +181,7 @@ function renderRunList() {
|
|||||||
<div class="tag-row" style="margin-top:10px;">
|
<div class="tag-row" style="margin-top:10px;">
|
||||||
<span class="tag">timeline ${escapeHtml(item.timeline?.length || 0)}</span>
|
<span class="tag">timeline ${escapeHtml(item.timeline?.length || 0)}</span>
|
||||||
<span class="tag">artifacts ${escapeHtml((item.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span>
|
<span class="tag">artifacts ${escapeHtml((item.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span>
|
||||||
<span class="tag">browser ${item.browser_evidence?.present ? "ready" : "missing"}</span>
|
<span class="tag">browser ${escapeHtml(browserLabel)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mini-muted" style="margin-top:10px;">${escapeHtml(reasoning)}</div>
|
<div class="mini-muted" style="margin-top:10px;">${escapeHtml(reasoning)}</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -234,7 +235,7 @@ function hydrateFilterOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaultArtifact(run) {
|
function defaultArtifact(run) {
|
||||||
const preference = ["requests", "container", "browser", "compose", "reports"];
|
const preference = ["attack", "requests", "container", "browser", "baseline", "compose", "reports"];
|
||||||
for (const key of preference) {
|
for (const key of preference) {
|
||||||
const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length);
|
const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length);
|
||||||
if (!group) continue;
|
if (!group) continue;
|
||||||
@@ -244,6 +245,73 @@ function defaultArtifact(run) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function totalProgress(progress) {
|
||||||
|
const values = Object.values(progress || {}).map((value) => Number(value || 0));
|
||||||
|
return values.reduce((sum, value) => sum + value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProgressStrip(progress) {
|
||||||
|
const total = totalProgress(progress);
|
||||||
|
if (!total) {
|
||||||
|
return `
|
||||||
|
<div class="progress-strip">
|
||||||
|
<div class="progress-bar"><div class="progress-segment progress-other" style="width:100%"></div></div>
|
||||||
|
<div class="mini-muted">No timeline progress recorded.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const order = [
|
||||||
|
["completed", "Completed", "progress-completed"],
|
||||||
|
["blocked", "Blocked", "progress-blocked"],
|
||||||
|
["failed", "Failed", "progress-failed"],
|
||||||
|
["skipped", "Skipped", "progress-skipped"],
|
||||||
|
["planned", "Planned", "progress-planned"],
|
||||||
|
["other", "Other", "progress-other"],
|
||||||
|
];
|
||||||
|
const segments = order
|
||||||
|
.filter(([key]) => Number(progress?.[key] || 0) > 0)
|
||||||
|
.map(([key, _label, klass]) => {
|
||||||
|
const count = Number(progress?.[key] || 0);
|
||||||
|
const pct = Math.max((count / total) * 100, 4);
|
||||||
|
return `<div class="progress-segment ${klass}" style="width:${pct}%"></div>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
const legend = order
|
||||||
|
.filter(([key]) => Number(progress?.[key] || 0) > 0)
|
||||||
|
.map(([key, label, klass]) => `
|
||||||
|
<span class="tag">
|
||||||
|
<span class="swatch ${klass}"></span>
|
||||||
|
${escapeHtml(label)} ${escapeHtml(progress?.[key] || 0)}
|
||||||
|
</span>
|
||||||
|
`)
|
||||||
|
.join("");
|
||||||
|
return `
|
||||||
|
<div class="progress-strip">
|
||||||
|
<div class="progress-bar">${segments}</div>
|
||||||
|
<div class="progress-legend">${legend}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStageCards(run) {
|
||||||
|
const timeline = run.timeline || [];
|
||||||
|
if (!timeline.length) {
|
||||||
|
return `<div class="empty-state">No stage records available.</div>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<div class="stage-grid">
|
||||||
|
${timeline.map((item) => `
|
||||||
|
<article class="stage-card">
|
||||||
|
<strong>${escapeHtml(item.step || "-")}</strong>
|
||||||
|
<div class="${statusClass(item.status || "default")}">${escapeHtml(item.status || "unknown")}</div>
|
||||||
|
<div class="mini-muted" style="margin-top:10px;">${escapeHtml(item.detail || "-")}</div>
|
||||||
|
<div class="mini-muted" style="margin-top:8px;">${escapeHtml(item.at || "-")}</div>
|
||||||
|
</article>
|
||||||
|
`).join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function openArtifact(href, label, kind) {
|
async function openArtifact(href, label, kind) {
|
||||||
state.selectedArtifact = { href, label, kind };
|
state.selectedArtifact = { href, label, kind };
|
||||||
document.querySelectorAll(".artifact-button").forEach((button) => {
|
document.querySelectorAll(".artifact-button").forEach((button) => {
|
||||||
@@ -300,6 +368,7 @@ function renderDetail() {
|
|||||||
<span class="tag">${escapeHtml(run.repro_profile_id)}</span>
|
<span class="tag">${escapeHtml(run.repro_profile_id)}</span>
|
||||||
<span class="tag">${escapeHtml(run.artifact_mode)}</span>
|
<span class="tag">${escapeHtml(run.artifact_mode)}</span>
|
||||||
<span class="tag">${escapeHtml(run.verification_mode)}</span>
|
<span class="tag">${escapeHtml(run.verification_mode)}</span>
|
||||||
|
<span class="tag">${escapeHtml(run.target_env || "local-docker")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="detail-headline">${escapeHtml(advisory.title || run.advisory_id)}</h2>
|
<h2 class="detail-headline">${escapeHtml(advisory.title || run.advisory_id)}</h2>
|
||||||
@@ -308,11 +377,12 @@ function renderDetail() {
|
|||||||
<a class="chip" href="${escapeHtml(run.dashboard_refs.report_html)}" target="_blank" rel="noreferrer">Open HTML report</a>
|
<a class="chip" href="${escapeHtml(run.dashboard_refs.report_html)}" target="_blank" rel="noreferrer">Open HTML report</a>
|
||||||
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.report_md)}" target="_blank" rel="noreferrer">Open Markdown</a>
|
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.report_md)}" target="_blank" rel="noreferrer">Open Markdown</a>
|
||||||
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.bundle)}" target="_blank" rel="noreferrer">Open run JSON</a>
|
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.bundle)}" target="_blank" rel="noreferrer">Open run JSON</a>
|
||||||
|
<a class="ghost-chip" href="./docs/frontend-dashboard-design.html" target="_blank" rel="noreferrer">Open UI spec</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-grid">
|
<div class="stat-grid">
|
||||||
<article class="stat-card"><strong>Timeline Steps</strong><span>${escapeHtml(run.timeline?.length || 0)}</span></article>
|
<article class="stat-card"><strong>Timeline Steps</strong><span>${escapeHtml(run.timeline?.length || 0)}</span></article>
|
||||||
<article class="stat-card"><strong>Artifacts</strong><span>${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span></article>
|
<article class="stat-card"><strong>Artifacts</strong><span>${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span></article>
|
||||||
<article class="stat-card"><strong>Browser</strong><span>${run.browser_evidence?.present ? "Ready" : "Missing"}</span></article>
|
<article class="stat-card"><strong>Browser</strong><span>${run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional")}</span></article>
|
||||||
<article class="stat-card"><strong>Finished</strong><span>${escapeHtml(timeAgo(run.finished_at))}</span></article>
|
<article class="stat-card"><strong>Finished</strong><span>${escapeHtml(timeAgo(run.finished_at))}</span></article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -322,12 +392,8 @@ function renderDetail() {
|
|||||||
<details class="glass-panel accordion" open>
|
<details class="glass-panel accordion" open>
|
||||||
<summary><span>Progress Timeline</span><span class="tag">${escapeHtml(run.timeline?.length || 0)} steps</span></summary>
|
<summary><span>Progress Timeline</span><span class="tag">${escapeHtml(run.timeline?.length || 0)} steps</span></summary>
|
||||||
<div class="accordion-content">
|
<div class="accordion-content">
|
||||||
<div class="tag-row" style="margin-bottom:14px;">
|
${renderProgressStrip(run.progress)}
|
||||||
<span class="tag">completed ${escapeHtml(run.progress?.completed || 0)}</span>
|
${renderStageCards(run)}
|
||||||
<span class="tag">blocked ${escapeHtml(run.progress?.blocked || 0)}</span>
|
|
||||||
<span class="tag">skipped ${escapeHtml(run.progress?.skipped || 0)}</span>
|
|
||||||
<span class="tag">failed ${escapeHtml(run.progress?.failed || 0)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-list">
|
<div class="timeline-list">
|
||||||
${(run.timeline || []).map((item) => `
|
${(run.timeline || []).map((item) => `
|
||||||
<article class="timeline-item">
|
<article class="timeline-item">
|
||||||
@@ -424,7 +490,7 @@ function renderDetail() {
|
|||||||
${(advisory.secondary_source_urls || []).map((ref) => `<a href="${escapeHtml(ref)}" target="_blank" rel="noreferrer">${escapeHtml(ref)}</a>`).join("")}
|
${(advisory.secondary_source_urls || []).map((ref) => `<a href="${escapeHtml(ref)}" target="_blank" rel="noreferrer">${escapeHtml(ref)}</a>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-row" style="margin-top:16px;">
|
<div class="tag-row" style="margin-top:16px;">
|
||||||
${(advisory.secure_code_topics || []).map((topic) => `<span class="tag">${escapeHtml(topic)}</span>`).join("")}
|
${(advisory.secure_code_topics || []).map((topic) => `<a class="tag" href="./docs/secure-code-index.html" target="_blank" rel="noreferrer">${escapeHtml(topic)}</a>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -400,6 +400,70 @@ button, input, select {
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-strip {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
border: 1px solid rgba(159, 179, 202, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment {
|
||||||
|
min-width: 10px;
|
||||||
|
transition: width 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-completed { background: linear-gradient(90deg, rgba(110, 231, 165, 0.9), rgba(94, 234, 212, 0.9)); }
|
||||||
|
.progress-blocked { background: linear-gradient(90deg, rgba(255, 123, 123, 0.95), rgba(255, 160, 122, 0.9)); }
|
||||||
|
.progress-failed { background: linear-gradient(90deg, rgba(255, 123, 123, 0.92), rgba(255, 209, 102, 0.88)); }
|
||||||
|
.progress-skipped { background: linear-gradient(90deg, rgba(255,255,255,0.22), rgba(159, 179, 202, 0.3)); }
|
||||||
|
.progress-planned { background: linear-gradient(90deg, rgba(144, 205, 244, 0.82), rgba(94, 234, 212, 0.72)); }
|
||||||
|
.progress-other { background: linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.1)); }
|
||||||
|
|
||||||
|
.progress-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-legend .tag {
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-legend .swatch {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-card {
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid rgba(159, 179, 202, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-card strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.accordion {
|
.accordion {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,402 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>本地前端工作台设计文档</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #08111f;
|
||||||
|
--panel: rgba(9, 18, 32, 0.9);
|
||||||
|
--border: rgba(137, 171, 214, 0.2);
|
||||||
|
--text: #f7fafc;
|
||||||
|
--muted: #9fb3ca;
|
||||||
|
--accent: #5eead4;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(94, 234, 212, 0.12), transparent 26%),
|
||||||
|
linear-gradient(160deg, #050c16 0%, #091526 50%, #10233d 100%);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 20px 40px;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 24px 80px rgba(1, 7, 20, 0.45);
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.chip:hover { border-color: rgba(94, 234, 212, 0.42); }
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-family: "IBM Plex Serif", Georgia, serif;
|
||||||
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||||
|
line-height: 1.08;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(137, 171, 214, 0.12);
|
||||||
|
background: rgba(2, 8, 22, 0.84);
|
||||||
|
color: #d6e5f5;
|
||||||
|
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="actions">
|
||||||
|
<a class="chip" href="../index.html">Back to dashboard</a>
|
||||||
|
</div>
|
||||||
|
<h1>本地前端工作台设计文档</h1>
|
||||||
|
<div class="meta">Dashboard-local mirror of the UI and interaction specification. | source: docs/frontend-dashboard-design.md</div>
|
||||||
|
<pre># 本地前端工作台设计文档
|
||||||
|
|
||||||
|
> `LAB ONLY` | `AUTHORIZED TARGETS ONLY`
|
||||||
|
|
||||||
|
## 1. 设计目标
|
||||||
|
|
||||||
|
本地 dashboard 要从“简单索引页”升级成“完整的授权攻防实证工作台”。它需要同时满足三类使用场景:
|
||||||
|
|
||||||
|
1. 运行中观察
|
||||||
|
- 看当前 run 进度
|
||||||
|
- 看失败原因和阻塞点
|
||||||
|
- 实时打开日志和证据
|
||||||
|
2. 复盘分析
|
||||||
|
- 查看 timeline、思路、利用路径、来源与修复主题
|
||||||
|
- 对比真实版本与 synthetic 复现差异
|
||||||
|
3. 审阅归档
|
||||||
|
- 从一个前端入口点进 `report.md`, `report.html`, `run.json`, 原始日志和截图
|
||||||
|
|
||||||
|
## 2. 页面定位
|
||||||
|
|
||||||
|
### 2.1 页面名称
|
||||||
|
|
||||||
|
- 页面名称:`Authorized Lab Dashboard`
|
||||||
|
- 页面语境:本地静态前端 + 本地文件 JSON 数据源
|
||||||
|
- 非目标:在线 SaaS、多用户后端、生产管理台
|
||||||
|
|
||||||
|
### 2.2 核心原则
|
||||||
|
|
||||||
|
- 所有展示都围绕授权目标
|
||||||
|
- 失败信息不能被隐藏在深层页面里
|
||||||
|
- 信息密度高,但必须可折叠、可筛选、可逐层展开
|
||||||
|
- 日志与原始 JSON 必须能直接预览
|
||||||
|
- 页面视觉应更生动,但不能牺牲扫描效率
|
||||||
|
|
||||||
|
## 3. 信息架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["Hero + Global Status"] --> B["Sidebar Filters"]
|
||||||
|
A --> C["Run Queue List"]
|
||||||
|
C --> D["Run Detail Hero"]
|
||||||
|
D --> E["Progress Timeline"]
|
||||||
|
D --> F["Attack Plan & Reasoning"]
|
||||||
|
D --> G["Evidence Explorer"]
|
||||||
|
D --> H["Live Log Viewer"]
|
||||||
|
D --> I["Sources & Fix Topics"]
|
||||||
|
D --> J["Raw JSON Panels"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 页面布局
|
||||||
|
|
||||||
|
### 4.1 顶部 Hero
|
||||||
|
|
||||||
|
必须展示:
|
||||||
|
|
||||||
|
- 页面名称
|
||||||
|
- 授权实验语境说明
|
||||||
|
- 刷新按钮
|
||||||
|
- 自动刷新开关
|
||||||
|
- 当前同步状态
|
||||||
|
- 核心 metric cards
|
||||||
|
|
||||||
|
视觉要求:
|
||||||
|
|
||||||
|
- 不能是纯白表格页
|
||||||
|
- 需要有分层背景、渐变光晕、轻微动态氛围
|
||||||
|
- 顶栏 sticky,滚动时仍可看到刷新和状态
|
||||||
|
|
||||||
|
### 4.2 左侧侧栏
|
||||||
|
|
||||||
|
包含四块:
|
||||||
|
|
||||||
|
- Filters
|
||||||
|
- 搜索
|
||||||
|
- system filter
|
||||||
|
- status filter
|
||||||
|
- profile filter
|
||||||
|
- Systems
|
||||||
|
- 系统覆盖度
|
||||||
|
- browser evidence 覆盖
|
||||||
|
- latest update
|
||||||
|
- Recent Failures
|
||||||
|
- 最近 blocker
|
||||||
|
- status
|
||||||
|
- 原因摘要
|
||||||
|
- Run Queue View
|
||||||
|
- 最近 run 卡片列表
|
||||||
|
- 可选中并切换到 detail panel
|
||||||
|
|
||||||
|
### 4.3 右侧 Detail Workspace
|
||||||
|
|
||||||
|
必须包含:
|
||||||
|
|
||||||
|
- Run Hero
|
||||||
|
- advisory 标题
|
||||||
|
- system / profile / artifact / verification 状态
|
||||||
|
- report / bundle / markdown 入口
|
||||||
|
- Progress Timeline
|
||||||
|
- 每一步的时间、状态、说明
|
||||||
|
- Attack Plan & Reasoning
|
||||||
|
- success criteria
|
||||||
|
- seed / attack notes
|
||||||
|
- failure reason
|
||||||
|
- 当前 blocker
|
||||||
|
- Evidence Explorer
|
||||||
|
- reports
|
||||||
|
- compose
|
||||||
|
- browser evidence
|
||||||
|
- request logs
|
||||||
|
- container logs
|
||||||
|
- Live Log Viewer
|
||||||
|
- 预览 text/json/html/image
|
||||||
|
- refresh preview
|
||||||
|
- open artifact
|
||||||
|
- Sources & Fix Topics
|
||||||
|
- official source
|
||||||
|
- secondary sources
|
||||||
|
- aliases
|
||||||
|
- secure code topics
|
||||||
|
- Raw JSON
|
||||||
|
- run JSON
|
||||||
|
- advisory JSON
|
||||||
|
- profile JSON
|
||||||
|
|
||||||
|
## 5. 交互要求
|
||||||
|
|
||||||
|
### 5.1 折叠 / 展开
|
||||||
|
|
||||||
|
所有 detail 分区都应支持折叠:
|
||||||
|
|
||||||
|
- Progress Timeline
|
||||||
|
- Attack Plan & Reasoning
|
||||||
|
- Evidence Explorer
|
||||||
|
- Live Log Viewer
|
||||||
|
- Sources & Fix Topics
|
||||||
|
- Raw JSON sections
|
||||||
|
|
||||||
|
折叠要求:
|
||||||
|
|
||||||
|
- 默认展开常用分区
|
||||||
|
- 次级原始数据可以默认收起
|
||||||
|
- 折叠状态视觉要清晰,不靠小箭头弱提示
|
||||||
|
|
||||||
|
### 5.2 Run 切换
|
||||||
|
|
||||||
|
- 点击左侧 run card 后,右侧 detail panel 即时刷新
|
||||||
|
- 当前选中项要有强视觉区别
|
||||||
|
- URL hash 应保留 `#run=<id>`,方便直接打开特定 run
|
||||||
|
|
||||||
|
### 5.3 Artifact 预览
|
||||||
|
|
||||||
|
点击 artifact button 后:
|
||||||
|
|
||||||
|
- JSON 自动格式化
|
||||||
|
- 日志文件以 `<pre>` 方式显示
|
||||||
|
- 图片以内联方式展示
|
||||||
|
- HTML 报告可 iframe 预览或新窗口打开
|
||||||
|
|
||||||
|
### 5.4 自动刷新
|
||||||
|
|
||||||
|
- 默认每 5 秒刷新一次 dashboard JSON
|
||||||
|
- 用户可以关闭自动刷新
|
||||||
|
- 当前正在查看的 artifact 在自动刷新开启时应支持重新抓取
|
||||||
|
|
||||||
|
### 5.5 失败原因高亮
|
||||||
|
|
||||||
|
对于 `blocked-*` 和 `triage-manual`:
|
||||||
|
|
||||||
|
- 顶部 hero 要显示状态 pill
|
||||||
|
- reasoning 面板要显示 failure callout
|
||||||
|
- 左侧 Recent Failures 要保留最近失败摘要
|
||||||
|
|
||||||
|
## 6. 展示字段清单
|
||||||
|
|
||||||
|
### 6.1 Hero 区
|
||||||
|
|
||||||
|
- run_id
|
||||||
|
- advisory_id
|
||||||
|
- advisory title
|
||||||
|
- verification_status
|
||||||
|
- verification_mode
|
||||||
|
- artifact_mode
|
||||||
|
- system_id
|
||||||
|
- repro_profile_id
|
||||||
|
- finished_at
|
||||||
|
|
||||||
|
### 6.2 Timeline 区
|
||||||
|
|
||||||
|
- `timeline[].at`
|
||||||
|
- `timeline[].step`
|
||||||
|
- `timeline[].status`
|
||||||
|
- `timeline[].detail`
|
||||||
|
|
||||||
|
### 6.3 Reasoning 区
|
||||||
|
|
||||||
|
- advisory summary
|
||||||
|
- profile seed messages
|
||||||
|
- profile attack messages
|
||||||
|
- profile success criteria
|
||||||
|
- blocked reason
|
||||||
|
|
||||||
|
### 6.4 Sources 区
|
||||||
|
|
||||||
|
- official_source_url
|
||||||
|
- secondary_source_urls
|
||||||
|
- aliases
|
||||||
|
- secure_code_topics
|
||||||
|
|
||||||
|
### 6.5 Evidence 区
|
||||||
|
|
||||||
|
- report.html
|
||||||
|
- report.md
|
||||||
|
- timeline.mmd
|
||||||
|
- bundle json
|
||||||
|
- compose.yaml
|
||||||
|
- browser screenshots / DOM / console / network
|
||||||
|
- request logs
|
||||||
|
- container logs
|
||||||
|
|
||||||
|
## 7. 动效与视觉要求
|
||||||
|
|
||||||
|
### 7.1 必须有的视觉增强
|
||||||
|
|
||||||
|
- 顶部背景渐变和环境光
|
||||||
|
- status pill 发光色彩区分
|
||||||
|
- 卡片 hover 浮起
|
||||||
|
- sticky hero
|
||||||
|
- 折叠面板开合层次
|
||||||
|
- gallery 缩略图点击查看
|
||||||
|
|
||||||
|
### 7.2 推荐但必须受控
|
||||||
|
|
||||||
|
- 状态小圆点 pulse
|
||||||
|
- 背景网格或轻微数据面纹理
|
||||||
|
- 面板玻璃感和浅透视阴影
|
||||||
|
|
||||||
|
### 7.3 不允许
|
||||||
|
|
||||||
|
- 花哨但影响可读性的动画
|
||||||
|
- 大面积纯装饰 3D 效果
|
||||||
|
- 自动播放噪音式动效
|
||||||
|
- 让日志区难以复制文本的视觉处理
|
||||||
|
|
||||||
|
## 8. 实时日志与细节查看要求
|
||||||
|
|
||||||
|
### 8.1 日志查看器
|
||||||
|
|
||||||
|
日志查看器必须支持:
|
||||||
|
|
||||||
|
- 选中文件后即刻预览
|
||||||
|
- JSON 格式化
|
||||||
|
- text/json/html/image 四类预览
|
||||||
|
- 打开原文件
|
||||||
|
- 在自动刷新开启时重新抓取当前文件
|
||||||
|
|
||||||
|
### 8.2 重点要看的日志
|
||||||
|
|
||||||
|
- compose / environment 文件
|
||||||
|
- baseline / attack / browser json
|
||||||
|
- container logs
|
||||||
|
- request logs
|
||||||
|
- timeline / bundle
|
||||||
|
|
||||||
|
### 8.3 失败排查导向
|
||||||
|
|
||||||
|
失败时应优先展示:
|
||||||
|
|
||||||
|
- `blocked_reason`
|
||||||
|
- 当前 step
|
||||||
|
- 上一个完成 step
|
||||||
|
- 当前可打开的日志 / 报告 / run bundle
|
||||||
|
- 对应 advisory 来源与 profile success criteria
|
||||||
|
|
||||||
|
## 9. 数据源契约
|
||||||
|
|
||||||
|
前端依赖的本地 JSON/文件源:
|
||||||
|
|
||||||
|
- `summary.json`
|
||||||
|
- `runs.json`
|
||||||
|
- `systems.json`
|
||||||
|
- `advisories.json`
|
||||||
|
- `profiles.json`
|
||||||
|
- `runs/<run-id>/report.html`
|
||||||
|
- `runs/<run-id>/report.md`
|
||||||
|
- `runs/<run-id>/run.json`
|
||||||
|
- `runs/<run-id>/logs/*`
|
||||||
|
- `runs/<run-id>/assets/*`
|
||||||
|
|
||||||
|
前端不直接写这些数据,只读取并展示。
|
||||||
|
|
||||||
|
## 10. 落地约束
|
||||||
|
|
||||||
|
- 保持静态前端,不引入长期运行后端
|
||||||
|
- 本地 `serve-dashboard` 即可查看
|
||||||
|
- 对于正在跑的 case,前端通过轮询读取新 JSON 实现“近实时”
|
||||||
|
- 不依赖第三方 CDN UI 库
|
||||||
|
- 优先使用原生 HTML/CSS/JS,可长期维护
|
||||||
|
|
||||||
|
## 11. 验收标准
|
||||||
|
|
||||||
|
页面完成后,应满足:
|
||||||
|
|
||||||
|
- 能从 run list 切换到 detail panel
|
||||||
|
- 能折叠与展开各信息区
|
||||||
|
- 能打开并预览 JSON / text / image / html artifact
|
||||||
|
- 能看到失败原因、思路、来源、修复主题
|
||||||
|
- 能筛选 system / status / profile
|
||||||
|
- 能在自动刷新开启时重新载入 dashboard 数据
|
||||||
|
- 页面视觉比“普通表格页”更生动,但仍适合高密度阅读
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>项目功能与特性总览</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #08111f;
|
||||||
|
--panel: rgba(9, 18, 32, 0.9);
|
||||||
|
--border: rgba(137, 171, 214, 0.2);
|
||||||
|
--text: #f7fafc;
|
||||||
|
--muted: #9fb3ca;
|
||||||
|
--accent: #5eead4;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(94, 234, 212, 0.12), transparent 26%),
|
||||||
|
linear-gradient(160deg, #050c16 0%, #091526 50%, #10233d 100%);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 20px 40px;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 24px 80px rgba(1, 7, 20, 0.45);
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.chip:hover { border-color: rgba(94, 234, 212, 0.42); }
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-family: "IBM Plex Serif", Georgia, serif;
|
||||||
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||||
|
line-height: 1.08;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(137, 171, 214, 0.12);
|
||||||
|
background: rgba(2, 8, 22, 0.84);
|
||||||
|
color: #d6e5f5;
|
||||||
|
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="actions">
|
||||||
|
<a class="chip" href="../index.html">Back to dashboard</a>
|
||||||
|
</div>
|
||||||
|
<h1>项目功能与特性总览</h1>
|
||||||
|
<div class="meta">Dashboard-local mirror of the repo feature guide. | source: docs/project-features.md</div>
|
||||||
|
<pre># 项目功能与特性总览
|
||||||
|
|
||||||
|
> `LAB ONLY` | `AUTHORIZED TARGETS ONLY`
|
||||||
|
|
||||||
|
## 1. 项目定位
|
||||||
|
|
||||||
|
`websafe` 是一套“授权攻防实验与研究知识库 + 本地实证系统”。它不是生产安全基线库,也不是面向任意第三方站点的扫描平台。
|
||||||
|
|
||||||
|
项目覆盖:
|
||||||
|
|
||||||
|
- 本地靶场、Docker 集群、内网实验节点
|
||||||
|
- 自建且可公网访问的测试网站、服务器、设备
|
||||||
|
- 已明确授权的验证性测试目标
|
||||||
|
|
||||||
|
项目不覆盖:
|
||||||
|
|
||||||
|
- 无归属证明、无授权的公网资产
|
||||||
|
- 公共知名网站
|
||||||
|
- 泛互联网画像、枚举、对外大规模探测
|
||||||
|
|
||||||
|
## 2. 功能版图
|
||||||
|
|
||||||
|
### 2.1 情报与入库
|
||||||
|
|
||||||
|
- `08-threat-intel/source-map.yaml`
|
||||||
|
- 定义系统范围、来源、覆盖策略、输出目录、secure-code 主题
|
||||||
|
- `08-threat-intel/repro-map.yaml`
|
||||||
|
- 定义系统到 repro family、浏览器要求、日志策略和报告模板的映射
|
||||||
|
- `08-threat-intel/repro-profiles/`
|
||||||
|
- family 级和 advisory 级复现说明
|
||||||
|
- `08-threat-intel/registry/`
|
||||||
|
- advisory、system、run、triage 的唯一真值层
|
||||||
|
- `08-threat-intel/generated/`
|
||||||
|
- coverage matrix、latest ingest、dashboard 等人类可读产物
|
||||||
|
|
||||||
|
### 2.2 本地实证与编排
|
||||||
|
|
||||||
|
- `00-environments/catalog/`
|
||||||
|
- 记录系统、镜像、源码、依赖和健康检查的 catalog
|
||||||
|
- `00-environments/profiles/`
|
||||||
|
- 记录具体版本或 current profile 的 compose / baseline / seed 参数
|
||||||
|
- `scripts/lab/main.py`
|
||||||
|
- 唯一 lab CLI 入口
|
||||||
|
- `scripts/lab/`
|
||||||
|
- `catalog`, `provision`, `compose`, `seed`, `baseline`, `attack`, `browser`, `evidence`, `render`, `queue`, `validators`
|
||||||
|
|
||||||
|
### 2.3 攻击验证工具
|
||||||
|
|
||||||
|
- `01-sql-injection/`
|
||||||
|
- `sqli-scanner.py`, `blind-sqli.py`, `sqli-exploit.go`
|
||||||
|
- `02-xss/`
|
||||||
|
- `xss-fuzzer.py`, `xss-scanner.go`
|
||||||
|
- `03-authentication/`
|
||||||
|
- `web-brute.py`, `jwt-cracker.py`, `session-lab.py`
|
||||||
|
- `04-server-security/`
|
||||||
|
- `port-scanner.py`, `tls-scanner.py`, `site-scope-mapper.py`, `misconfig-lab.py`
|
||||||
|
|
||||||
|
### 2.4 结果展示
|
||||||
|
|
||||||
|
- `06-case-studies/generated-runs/<run-id>/`
|
||||||
|
- `report.md`, `report.html`, `timeline.mmd`, `assets/`, `logs/`
|
||||||
|
- `08-threat-intel/generated/dashboard/`
|
||||||
|
- 静态前端工作台
|
||||||
|
- `07-framework-security/`
|
||||||
|
- 系统级 README、INDEX、案例页,自动显示本地实证状态
|
||||||
|
|
||||||
|
## 3. 数据流与自动化链路
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["Threat Intel Sources"] --> B["registry/advisories"]
|
||||||
|
B --> C["repro-map + repro-profiles"]
|
||||||
|
C --> D["00-environments catalog/profiles"]
|
||||||
|
D --> E["scripts/lab run-case / run-batch"]
|
||||||
|
E --> F["generated-runs/<run-id>"]
|
||||||
|
F --> G["registry/runs"]
|
||||||
|
G --> H["case pages / system INDEX"]
|
||||||
|
G --> I["dashboard JSON + local UI"]
|
||||||
|
H --> J["README / docs / PR"]
|
||||||
|
I --> J
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 关键特性
|
||||||
|
|
||||||
|
### 4.1 完整覆盖语义
|
||||||
|
|
||||||
|
- 每条 advisory 至少进入 `registry/advisories`
|
||||||
|
- 每条 advisory 必须有明确的实证状态
|
||||||
|
- 状态只允许:
|
||||||
|
- `verified-real`
|
||||||
|
- `verified-synthetic`
|
||||||
|
- `blocked-artifact`
|
||||||
|
- `blocked-destructive`
|
||||||
|
- `triage-manual`
|
||||||
|
|
||||||
|
### 4.2 浏览器证据强制
|
||||||
|
|
||||||
|
- XSS、DOM XSS、Token 存储、前端路由绕过、前端配置暴露等浏览器类 case
|
||||||
|
- 必须生成截图
|
||||||
|
- 必须生成 DOM 快照
|
||||||
|
- 必须生成 console / network 证据
|
||||||
|
- 没有浏览器证据不得升级为 `verified-*`
|
||||||
|
|
||||||
|
### 4.3 受控攻击语义
|
||||||
|
|
||||||
|
- 默认模式是 `minimal-proof`
|
||||||
|
- 只读探测、最小化注入、可审计回显、可回滚验证
|
||||||
|
- 破坏性利用、越权下载真实数据、不可回滚行为默认禁用
|
||||||
|
|
||||||
|
### 4.4 双展示面
|
||||||
|
|
||||||
|
- 静态归档报告
|
||||||
|
- 适合证据留存、归档、PR 审阅
|
||||||
|
- 本地前端工作台
|
||||||
|
- 适合实时查看进度、日志、失败原因、来源、思路、截图和原始 JSON
|
||||||
|
|
||||||
|
### 4.5 自动化提交
|
||||||
|
|
||||||
|
- `scripts/intel/run-hourly.sh`
|
||||||
|
- hotlane ingest + hotlane repro
|
||||||
|
- `scripts/intel/run-nightly.sh`
|
||||||
|
- 常规 ingest + batch repro + render + validate + PR
|
||||||
|
- `scripts/intel/run-weekly-reconcile.sh`
|
||||||
|
- reconcile + retry failures + rerender + validate + PR
|
||||||
|
|
||||||
|
## 5. CLI 能力
|
||||||
|
|
||||||
|
### 5.1 Intel CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 /Users/x/websafe/scripts/intel/main.py hotlane
|
||||||
|
python3 /Users/x/websafe/scripts/intel/main.py ingest --since last-success
|
||||||
|
python3 /Users/x/websafe/scripts/intel/main.py reconcile
|
||||||
|
python3 /Users/x/websafe/scripts/intel/main.py render
|
||||||
|
python3 /Users/x/websafe/scripts/intel/main.py validate
|
||||||
|
python3 /Users/x/websafe/scripts/intel/main.py open-pr --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Lab CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 /Users/x/websafe/scripts/lab/main.py catalog sync
|
||||||
|
python3 /Users/x/websafe/scripts/lab/main.py validate
|
||||||
|
python3 /Users/x/websafe/scripts/lab/main.py run-case --case gitea--CVE-2025-68939
|
||||||
|
python3 /Users/x/websafe/scripts/lab/main.py run-case --case nextjs--CVE-2025-29927 --dry-run
|
||||||
|
python3 /Users/x/websafe/scripts/lab/main.py run-batch --limit 10
|
||||||
|
python3 /Users/x/websafe/scripts/lab/main.py serve-dashboard --port 8734
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 前端工作台当前目标
|
||||||
|
|
||||||
|
前端不只是“一个结果页”,而是本地实验控制台与证据阅读器。它需要:
|
||||||
|
|
||||||
|
- 快速定位系统 / advisory / repro profile
|
||||||
|
- 折叠与展开 timeline、evidence、sources、raw JSON
|
||||||
|
- 直接查看 compose、JSON、日志、截图、报告
|
||||||
|
- 高亮失败原因、当前 blocker、利用思路、成功判据
|
||||||
|
- 自动刷新生成数据,适配正在进行中的本地 run
|
||||||
|
|
||||||
|
详细设计见:
|
||||||
|
|
||||||
|
- [本地前端工作台设计文档](/Users/x/websafe/docs/frontend-dashboard-design.md)
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>安全编码修复库索引</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #08111f;
|
||||||
|
--panel: rgba(9, 18, 32, 0.9);
|
||||||
|
--border: rgba(137, 171, 214, 0.2);
|
||||||
|
--text: #f7fafc;
|
||||||
|
--muted: #9fb3ca;
|
||||||
|
--accent: #5eead4;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(94, 234, 212, 0.12), transparent 26%),
|
||||||
|
linear-gradient(160deg, #050c16 0%, #091526 50%, #10233d 100%);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 20px 40px;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 24px 80px rgba(1, 7, 20, 0.45);
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.chip:hover { border-color: rgba(94, 234, 212, 0.42); }
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-family: "IBM Plex Serif", Georgia, serif;
|
||||||
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||||
|
line-height: 1.08;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(137, 171, 214, 0.12);
|
||||||
|
background: rgba(2, 8, 22, 0.84);
|
||||||
|
color: #d6e5f5;
|
||||||
|
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="actions">
|
||||||
|
<a class="chip" href="../index.html">Back to dashboard</a>
|
||||||
|
</div>
|
||||||
|
<h1>安全编码修复库索引</h1>
|
||||||
|
<div class="meta">Dashboard-local mirror of the secure-code library index. | source: 05-defense/secure-code/INDEX.md</div>
|
||||||
|
<pre># 安全编码修复库
|
||||||
|
|
||||||
|
> `LAB ONLY` | 修复主题用于把实验发现映射回代码整改,不代表默认生产基线。
|
||||||
|
|
||||||
|
- 语言范围: `javascript-typescript`, `nodejs`, `java`, `php`, `python`, `ruby`, `csharp`, `go`
|
||||||
|
- 主题范围: 输出编码、DOM sink、CSP / Trusted Types、令牌存储、鉴权复核、SSRF、走私边界、路径穿越、文件上传、插件信任、依赖升级、代理信任、反序列化、模板注入。
|
||||||
|
|
||||||
|
- [javascript-typescript](/Users/x/websafe/05-defense/secure-code/javascript-typescript/README.md)
|
||||||
|
- [nodejs](/Users/x/websafe/05-defense/secure-code/nodejs/README.md)
|
||||||
|
- [java](/Users/x/websafe/05-defense/secure-code/java/README.md)
|
||||||
|
- [php](/Users/x/websafe/05-defense/secure-code/php/README.md)
|
||||||
|
- [python](/Users/x/websafe/05-defense/secure-code/python/README.md)
|
||||||
|
- [ruby](/Users/x/websafe/05-defense/secure-code/ruby/README.md)
|
||||||
|
- [csharp](/Users/x/websafe/05-defense/secure-code/csharp/README.md)
|
||||||
|
- [go](/Users/x/websafe/05-defense/secure-code/go/README.md)
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<button id="refreshDashboard" class="chip" type="button">Refresh Dashboard</button>
|
<button id="refreshDashboard" class="chip" type="button">Refresh Dashboard</button>
|
||||||
<label class="ghost-chip"><input id="autoRefresh" type="checkbox" checked> Auto Refresh</label>
|
<label class="ghost-chip"><input id="autoRefresh" type="checkbox" checked> Auto Refresh</label>
|
||||||
<a class="ghost-chip" href="./summary.json" target="_blank" rel="noreferrer">Open Summary JSON</a>
|
<a class="ghost-chip" href="./summary.json" target="_blank" rel="noreferrer">Open Summary JSON</a>
|
||||||
|
<a class="ghost-chip" href="./docs/project-features.html" target="_blank" rel="noreferrer">Open Feature Docs</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -272,6 +272,11 @@
|
|||||||
"timeline": "./runs/gitea-gitea--CVE-2025-68939-20260317063330/timeline.mmd",
|
"timeline": "./runs/gitea-gitea--CVE-2025-68939-20260317063330/timeline.mmd",
|
||||||
"bundle": "./runs/gitea-gitea--CVE-2025-68939-20260317063330/run.json"
|
"bundle": "./runs/gitea-gitea--CVE-2025-68939-20260317063330/run.json"
|
||||||
},
|
},
|
||||||
|
"browser_evidence": {
|
||||||
|
"required": false,
|
||||||
|
"present": false,
|
||||||
|
"refs": []
|
||||||
|
},
|
||||||
"browser_links": [],
|
"browser_links": [],
|
||||||
"container_links": [],
|
"container_links": [],
|
||||||
"request_links": [
|
"request_links": [
|
||||||
@@ -462,6 +467,11 @@
|
|||||||
"timeline": "./runs/nextjs-nextjs--CVE-2025-29927-20260317063047/timeline.mmd",
|
"timeline": "./runs/nextjs-nextjs--CVE-2025-29927-20260317063047/timeline.mmd",
|
||||||
"bundle": "./runs/nextjs-nextjs--CVE-2025-29927-20260317063047/run.json"
|
"bundle": "./runs/nextjs-nextjs--CVE-2025-29927-20260317063047/run.json"
|
||||||
},
|
},
|
||||||
|
"browser_evidence": {
|
||||||
|
"required": false,
|
||||||
|
"present": false,
|
||||||
|
"refs": []
|
||||||
|
},
|
||||||
"browser_links": [],
|
"browser_links": [],
|
||||||
"container_links": [],
|
"container_links": [],
|
||||||
"request_links": [
|
"request_links": [
|
||||||
@@ -591,20 +601,27 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "baseline",
|
||||||
|
"label": "Baseline Snapshots",
|
||||||
|
"count": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"href": "./runs/nextjs-nextjs--CVE-2025-29927-20260317063047/logs/baseline.json",
|
||||||
|
"label": "baseline.json",
|
||||||
|
"kind": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "requests",
|
"key": "requests",
|
||||||
"label": "Request Logs",
|
"label": "Request Logs",
|
||||||
"count": 2,
|
"count": 1,
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"href": "./runs/nextjs-nextjs--CVE-2025-29927-20260317063047/logs/attack.json",
|
"href": "./runs/nextjs-nextjs--CVE-2025-29927-20260317063047/logs/attack.json",
|
||||||
"label": "attack.json",
|
"label": "attack.json",
|
||||||
"kind": "text"
|
"kind": "text"
|
||||||
},
|
|
||||||
{
|
|
||||||
"href": "./runs/nextjs-nextjs--CVE-2025-29927-20260317063047/logs/baseline.json",
|
|
||||||
"label": "baseline.json",
|
|
||||||
"kind": "text"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-03-17T07:27:25+00:00",
|
"generated_at": "2026-03-17T07:36:43+00:00",
|
||||||
"advisory_count": 89,
|
"advisory_count": 89,
|
||||||
"run_count": 3,
|
"run_count": 3,
|
||||||
"statuses": {
|
"statuses": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 最新同步摘要
|
# 最新同步摘要
|
||||||
|
|
||||||
- 渲染时间: `2026-03-17T07:27:25+00:00`
|
- 渲染时间: `2026-03-17T07:36:11+00:00`
|
||||||
- 系统数量: `62`
|
- 系统数量: `62`
|
||||||
- Advisory 数量: `89`
|
- Advisory 数量: `89`
|
||||||
- 重点 Markdown 数量: `89`
|
- 重点 Markdown 数量: `89`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-03-17T07:27:25+00:00",
|
"generated_at": "2026-03-17T07:36:11+00:00",
|
||||||
"system_count": 62,
|
"system_count": 62,
|
||||||
"advisory_count": 89,
|
"advisory_count": 89,
|
||||||
"markdown_count": 89,
|
"markdown_count": 89,
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ def validate(source_map: Dict[str, Any]) -> List[str]:
|
|||||||
GENERATED_DIR / "dashboard" / "profiles.json",
|
GENERATED_DIR / "dashboard" / "profiles.json",
|
||||||
GENERATED_DIR / "dashboard" / "assets" / "app.js",
|
GENERATED_DIR / "dashboard" / "assets" / "app.js",
|
||||||
GENERATED_DIR / "dashboard" / "assets" / "styles.css",
|
GENERATED_DIR / "dashboard" / "assets" / "styles.css",
|
||||||
|
GENERATED_DIR / "dashboard" / "docs" / "project-features.html",
|
||||||
|
GENERATED_DIR / "dashboard" / "docs" / "frontend-dashboard-design.html",
|
||||||
|
GENERATED_DIR / "dashboard" / "docs" / "secure-code-index.html",
|
||||||
ROOT / "08-threat-intel" / "registry" / "source-confidence.md",
|
ROOT / "08-threat-intel" / "registry" / "source-confidence.md",
|
||||||
]:
|
]:
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from lab.config import ADVISORIES_DIR, CASE_RUNS_DIR, DASHBOARD_DIR, RUNS_DIR
|
from lab.config import ADVISORIES_DIR, CASE_RUNS_DIR, DASHBOARD_DIR, ROOT, RUNS_DIR
|
||||||
from lab.repro import load_profiles
|
from lab.repro import load_profiles
|
||||||
from lab.utils import ensure_dir, isoformat, load_json_dir, now_utc, unique, write_json, write_text
|
from lab.utils import ensure_dir, isoformat, load_json_dir, now_utc, unique, write_json, write_text
|
||||||
|
|
||||||
@@ -72,6 +72,15 @@ def _artifact_group(run: Dict[str, Any], key: str, label: str, refs: List[str],
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _attack_result_refs(run: Dict[str, Any]) -> List[str]:
|
||||||
|
refs: List[str] = []
|
||||||
|
for step in run.get("attack_steps", []):
|
||||||
|
result_path = step.get("result_path")
|
||||||
|
if result_path:
|
||||||
|
refs.append(str(result_path))
|
||||||
|
return unique(refs)
|
||||||
|
|
||||||
|
|
||||||
def _progress_counts(run: Dict[str, Any]) -> Dict[str, int]:
|
def _progress_counts(run: Dict[str, Any]) -> Dict[str, int]:
|
||||||
counts = {"completed": 0, "skipped": 0, "failed": 0, "blocked": 0, "planned": 0, "other": 0}
|
counts = {"completed": 0, "skipped": 0, "failed": 0, "blocked": 0, "planned": 0, "other": 0}
|
||||||
for item in run.get("timeline", []):
|
for item in run.get("timeline", []):
|
||||||
@@ -148,6 +157,132 @@ def _reasoning_lines(advisory: Dict[str, Any], profile: Dict[str, Any]) -> List[
|
|||||||
return unique(notes)
|
return unique(notes)
|
||||||
|
|
||||||
|
|
||||||
|
def _dashboard_doc_page(title: str, source_path: Path, description: str) -> str:
|
||||||
|
source_label = source_path.relative_to(ROOT) if source_path.is_relative_to(ROOT) else source_path
|
||||||
|
body = source_path.read_text(encoding="utf-8") if source_path.exists() else f"missing source: {source_path}"
|
||||||
|
return f"""<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{html.escape(title)}</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
--bg: #08111f;
|
||||||
|
--panel: rgba(9, 18, 32, 0.9);
|
||||||
|
--border: rgba(137, 171, 214, 0.2);
|
||||||
|
--text: #f7fafc;
|
||||||
|
--muted: #9fb3ca;
|
||||||
|
--accent: #5eead4;
|
||||||
|
}}
|
||||||
|
* {{ box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(94, 234, 212, 0.12), transparent 26%),
|
||||||
|
linear-gradient(160deg, #050c16 0%, #091526 50%, #10233d 100%);
|
||||||
|
}}
|
||||||
|
main {{
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 32px 20px 40px;
|
||||||
|
}}
|
||||||
|
.panel {{
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 24px 80px rgba(1, 7, 20, 0.45);
|
||||||
|
}}
|
||||||
|
.actions {{
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}}
|
||||||
|
.chip {{
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
text-decoration: none;
|
||||||
|
}}
|
||||||
|
.chip:hover {{ border-color: rgba(94, 234, 212, 0.42); }}
|
||||||
|
h1 {{
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-family: "IBM Plex Serif", Georgia, serif;
|
||||||
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||||
|
line-height: 1.08;
|
||||||
|
}}
|
||||||
|
.meta {{
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}}
|
||||||
|
pre {{
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(137, 171, 214, 0.12);
|
||||||
|
background: rgba(2, 8, 22, 0.84);
|
||||||
|
color: #d6e5f5;
|
||||||
|
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="actions">
|
||||||
|
<a class="chip" href="../index.html">Back to dashboard</a>
|
||||||
|
</div>
|
||||||
|
<h1>{html.escape(title)}</h1>
|
||||||
|
<div class="meta">{html.escape(description)} | source: {html.escape(str(source_label))}</div>
|
||||||
|
<pre>{html.escape(body)}</pre>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _write_dashboard_docs() -> None:
|
||||||
|
docs_dir = DASHBOARD_DIR / "docs"
|
||||||
|
ensure_dir(docs_dir)
|
||||||
|
docs = [
|
||||||
|
(
|
||||||
|
"project-features.html",
|
||||||
|
"项目功能与特性总览",
|
||||||
|
ROOT / "docs" / "project-features.md",
|
||||||
|
"Dashboard-local mirror of the repo feature guide.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"frontend-dashboard-design.html",
|
||||||
|
"本地前端工作台设计文档",
|
||||||
|
ROOT / "docs" / "frontend-dashboard-design.md",
|
||||||
|
"Dashboard-local mirror of the UI and interaction specification.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"secure-code-index.html",
|
||||||
|
"安全编码修复库索引",
|
||||||
|
ROOT / "05-defense" / "secure-code" / "INDEX.md",
|
||||||
|
"Dashboard-local mirror of the secure-code library index.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for filename, title, source_path, description in docs:
|
||||||
|
write_text(docs_dir / filename, _dashboard_doc_page(title, source_path, description))
|
||||||
|
|
||||||
|
|
||||||
def render_run(run: Dict[str, Any]) -> Dict[str, str]:
|
def render_run(run: Dict[str, Any]) -> Dict[str, str]:
|
||||||
run_dir = CASE_RUNS_DIR / run["run_id"]
|
run_dir = CASE_RUNS_DIR / run["run_id"]
|
||||||
ensure_dir(run_dir / "assets")
|
ensure_dir(run_dir / "assets")
|
||||||
@@ -310,6 +445,7 @@ def render_run(run: Dict[str, Any]) -> Dict[str, str]:
|
|||||||
|
|
||||||
def render_dashboard() -> Dict[str, str]:
|
def render_dashboard() -> Dict[str, str]:
|
||||||
ensure_dir(DASHBOARD_DIR)
|
ensure_dir(DASHBOARD_DIR)
|
||||||
|
_write_dashboard_docs()
|
||||||
advisory_records = load_json_dir(ADVISORIES_DIR)
|
advisory_records = load_json_dir(ADVISORIES_DIR)
|
||||||
runs = load_json_dir(RUNS_DIR)
|
runs = load_json_dir(RUNS_DIR)
|
||||||
advisory_map = {item["canonical_id"]: item for item in advisory_records if item.get("canonical_id")}
|
advisory_map = {item["canonical_id"]: item for item in advisory_records if item.get("canonical_id")}
|
||||||
@@ -378,12 +514,19 @@ def render_dashboard() -> Dict[str, str]:
|
|||||||
cloned = dict(item)
|
cloned = dict(item)
|
||||||
advisory = advisory_map.get(item["advisory_id"], {})
|
advisory = advisory_map.get(item["advisory_id"], {})
|
||||||
profile = profile_map.get(item["repro_profile_id"], {})
|
profile = profile_map.get(item["repro_profile_id"], {})
|
||||||
|
browser_evidence = item.get("browser_evidence") or advisory.get("browser_evidence") or {
|
||||||
|
"required": bool(profile.get("browser_assertions", {}).get("required")),
|
||||||
|
"present": False,
|
||||||
|
"refs": [],
|
||||||
|
}
|
||||||
|
request_only_refs = [ref for ref in item.get("request_log_refs", []) if ref not in item.get("baseline_refs", [])]
|
||||||
cloned["dashboard_refs"] = {
|
cloned["dashboard_refs"] = {
|
||||||
"report_html": f"./runs/{item['run_id']}/report.html",
|
"report_html": f"./runs/{item['run_id']}/report.html",
|
||||||
"report_md": f"./runs/{item['run_id']}/report.md",
|
"report_md": f"./runs/{item['run_id']}/report.md",
|
||||||
"timeline": f"./runs/{item['run_id']}/timeline.mmd",
|
"timeline": f"./runs/{item['run_id']}/timeline.mmd",
|
||||||
"bundle": f"./runs/{item['run_id']}/run.json",
|
"bundle": f"./runs/{item['run_id']}/run.json",
|
||||||
}
|
}
|
||||||
|
cloned["browser_evidence"] = browser_evidence
|
||||||
cloned["browser_links"] = [_dashboard_ref(item, ref) for ref in item.get("browser_refs", [])]
|
cloned["browser_links"] = [_dashboard_ref(item, ref) for ref in item.get("browser_refs", [])]
|
||||||
cloned["container_links"] = [_dashboard_ref(item, ref) for ref in item.get("container_log_refs", [])]
|
cloned["container_links"] = [_dashboard_ref(item, ref) for ref in item.get("container_log_refs", [])]
|
||||||
cloned["request_links"] = [_dashboard_ref(item, ref) for ref in item.get("request_log_refs", [])]
|
cloned["request_links"] = [_dashboard_ref(item, ref) for ref in item.get("request_log_refs", [])]
|
||||||
@@ -405,9 +548,11 @@ def render_dashboard() -> Dict[str, str]:
|
|||||||
use_dashboard_refs=True,
|
use_dashboard_refs=True,
|
||||||
),
|
),
|
||||||
_artifact_group(item, "compose", "Compose", item.get("compose_refs", [])),
|
_artifact_group(item, "compose", "Compose", item.get("compose_refs", [])),
|
||||||
|
_artifact_group(item, "baseline", "Baseline Snapshots", item.get("baseline_refs", [])),
|
||||||
|
_artifact_group(item, "attack", "Attack Outputs", _attack_result_refs(item)),
|
||||||
_artifact_group(item, "browser", "Browser Evidence", item.get("browser_refs", [])),
|
_artifact_group(item, "browser", "Browser Evidence", item.get("browser_refs", [])),
|
||||||
_artifact_group(item, "container", "Container Logs", item.get("container_log_refs", [])),
|
_artifact_group(item, "container", "Container Logs", item.get("container_log_refs", [])),
|
||||||
_artifact_group(item, "requests", "Request Logs", item.get("request_log_refs", [])),
|
_artifact_group(item, "requests", "Request Logs", request_only_refs),
|
||||||
]
|
]
|
||||||
cloned["artifact_groups"] = [group for group in cloned["artifact_groups"] if group["count"]]
|
cloned["artifact_groups"] = [group for group in cloned["artifact_groups"] if group["count"]]
|
||||||
decorated_runs.append(cloned)
|
decorated_runs.append(cloned)
|
||||||
@@ -842,6 +987,70 @@ button, input, select {
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-strip {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
border: 1px solid rgba(159, 179, 202, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-segment {
|
||||||
|
min-width: 10px;
|
||||||
|
transition: width 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-completed { background: linear-gradient(90deg, rgba(110, 231, 165, 0.9), rgba(94, 234, 212, 0.9)); }
|
||||||
|
.progress-blocked { background: linear-gradient(90deg, rgba(255, 123, 123, 0.95), rgba(255, 160, 122, 0.9)); }
|
||||||
|
.progress-failed { background: linear-gradient(90deg, rgba(255, 123, 123, 0.92), rgba(255, 209, 102, 0.88)); }
|
||||||
|
.progress-skipped { background: linear-gradient(90deg, rgba(255,255,255,0.22), rgba(159, 179, 202, 0.3)); }
|
||||||
|
.progress-planned { background: linear-gradient(90deg, rgba(144, 205, 244, 0.82), rgba(94, 234, 212, 0.72)); }
|
||||||
|
.progress-other { background: linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.1)); }
|
||||||
|
|
||||||
|
.progress-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-legend .tag {
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-legend .swatch {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-card {
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid rgba(159, 179, 202, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-card strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.accordion {
|
.accordion {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -1278,6 +1487,7 @@ function renderRunList() {
|
|||||||
const active = item.run_id === state.selectedRunId ? "is-active" : "";
|
const active = item.run_id === state.selectedRunId ? "is-active" : "";
|
||||||
const title = item.advisory_meta?.title || item.advisory_id;
|
const title = item.advisory_meta?.title || item.advisory_id;
|
||||||
const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || "";
|
const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || "";
|
||||||
|
const browserLabel = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "n/a");
|
||||||
return `
|
return `
|
||||||
<button class="run-card ${active}" data-run-id="${escapeHtml(item.run_id)}">
|
<button class="run-card ${active}" data-run-id="${escapeHtml(item.run_id)}">
|
||||||
<div class="run-card-top">
|
<div class="run-card-top">
|
||||||
@@ -1289,7 +1499,7 @@ function renderRunList() {
|
|||||||
<div class="tag-row" style="margin-top:10px;">
|
<div class="tag-row" style="margin-top:10px;">
|
||||||
<span class="tag">timeline ${escapeHtml(item.timeline?.length || 0)}</span>
|
<span class="tag">timeline ${escapeHtml(item.timeline?.length || 0)}</span>
|
||||||
<span class="tag">artifacts ${escapeHtml((item.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span>
|
<span class="tag">artifacts ${escapeHtml((item.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span>
|
||||||
<span class="tag">browser ${item.browser_evidence?.present ? "ready" : "missing"}</span>
|
<span class="tag">browser ${escapeHtml(browserLabel)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mini-muted" style="margin-top:10px;">${escapeHtml(reasoning)}</div>
|
<div class="mini-muted" style="margin-top:10px;">${escapeHtml(reasoning)}</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -1343,7 +1553,7 @@ function hydrateFilterOptions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaultArtifact(run) {
|
function defaultArtifact(run) {
|
||||||
const preference = ["requests", "container", "browser", "compose", "reports"];
|
const preference = ["attack", "requests", "container", "browser", "baseline", "compose", "reports"];
|
||||||
for (const key of preference) {
|
for (const key of preference) {
|
||||||
const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length);
|
const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length);
|
||||||
if (!group) continue;
|
if (!group) continue;
|
||||||
@@ -1353,6 +1563,73 @@ function defaultArtifact(run) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function totalProgress(progress) {
|
||||||
|
const values = Object.values(progress || {}).map((value) => Number(value || 0));
|
||||||
|
return values.reduce((sum, value) => sum + value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProgressStrip(progress) {
|
||||||
|
const total = totalProgress(progress);
|
||||||
|
if (!total) {
|
||||||
|
return `
|
||||||
|
<div class="progress-strip">
|
||||||
|
<div class="progress-bar"><div class="progress-segment progress-other" style="width:100%"></div></div>
|
||||||
|
<div class="mini-muted">No timeline progress recorded.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const order = [
|
||||||
|
["completed", "Completed", "progress-completed"],
|
||||||
|
["blocked", "Blocked", "progress-blocked"],
|
||||||
|
["failed", "Failed", "progress-failed"],
|
||||||
|
["skipped", "Skipped", "progress-skipped"],
|
||||||
|
["planned", "Planned", "progress-planned"],
|
||||||
|
["other", "Other", "progress-other"],
|
||||||
|
];
|
||||||
|
const segments = order
|
||||||
|
.filter(([key]) => Number(progress?.[key] || 0) > 0)
|
||||||
|
.map(([key, _label, klass]) => {
|
||||||
|
const count = Number(progress?.[key] || 0);
|
||||||
|
const pct = Math.max((count / total) * 100, 4);
|
||||||
|
return `<div class="progress-segment ${klass}" style="width:${pct}%"></div>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
const legend = order
|
||||||
|
.filter(([key]) => Number(progress?.[key] || 0) > 0)
|
||||||
|
.map(([key, label, klass]) => `
|
||||||
|
<span class="tag">
|
||||||
|
<span class="swatch ${klass}"></span>
|
||||||
|
${escapeHtml(label)} ${escapeHtml(progress?.[key] || 0)}
|
||||||
|
</span>
|
||||||
|
`)
|
||||||
|
.join("");
|
||||||
|
return `
|
||||||
|
<div class="progress-strip">
|
||||||
|
<div class="progress-bar">${segments}</div>
|
||||||
|
<div class="progress-legend">${legend}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStageCards(run) {
|
||||||
|
const timeline = run.timeline || [];
|
||||||
|
if (!timeline.length) {
|
||||||
|
return `<div class="empty-state">No stage records available.</div>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<div class="stage-grid">
|
||||||
|
${timeline.map((item) => `
|
||||||
|
<article class="stage-card">
|
||||||
|
<strong>${escapeHtml(item.step || "-")}</strong>
|
||||||
|
<div class="${statusClass(item.status || "default")}">${escapeHtml(item.status || "unknown")}</div>
|
||||||
|
<div class="mini-muted" style="margin-top:10px;">${escapeHtml(item.detail || "-")}</div>
|
||||||
|
<div class="mini-muted" style="margin-top:8px;">${escapeHtml(item.at || "-")}</div>
|
||||||
|
</article>
|
||||||
|
`).join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function openArtifact(href, label, kind) {
|
async function openArtifact(href, label, kind) {
|
||||||
state.selectedArtifact = { href, label, kind };
|
state.selectedArtifact = { href, label, kind };
|
||||||
document.querySelectorAll(".artifact-button").forEach((button) => {
|
document.querySelectorAll(".artifact-button").forEach((button) => {
|
||||||
@@ -1409,6 +1686,7 @@ function renderDetail() {
|
|||||||
<span class="tag">${escapeHtml(run.repro_profile_id)}</span>
|
<span class="tag">${escapeHtml(run.repro_profile_id)}</span>
|
||||||
<span class="tag">${escapeHtml(run.artifact_mode)}</span>
|
<span class="tag">${escapeHtml(run.artifact_mode)}</span>
|
||||||
<span class="tag">${escapeHtml(run.verification_mode)}</span>
|
<span class="tag">${escapeHtml(run.verification_mode)}</span>
|
||||||
|
<span class="tag">${escapeHtml(run.target_env || "local-docker")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="detail-headline">${escapeHtml(advisory.title || run.advisory_id)}</h2>
|
<h2 class="detail-headline">${escapeHtml(advisory.title || run.advisory_id)}</h2>
|
||||||
@@ -1417,11 +1695,12 @@ function renderDetail() {
|
|||||||
<a class="chip" href="${escapeHtml(run.dashboard_refs.report_html)}" target="_blank" rel="noreferrer">Open HTML report</a>
|
<a class="chip" href="${escapeHtml(run.dashboard_refs.report_html)}" target="_blank" rel="noreferrer">Open HTML report</a>
|
||||||
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.report_md)}" target="_blank" rel="noreferrer">Open Markdown</a>
|
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.report_md)}" target="_blank" rel="noreferrer">Open Markdown</a>
|
||||||
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.bundle)}" target="_blank" rel="noreferrer">Open run JSON</a>
|
<a class="ghost-chip" href="${escapeHtml(run.dashboard_refs.bundle)}" target="_blank" rel="noreferrer">Open run JSON</a>
|
||||||
|
<a class="ghost-chip" href="./docs/frontend-dashboard-design.html" target="_blank" rel="noreferrer">Open UI spec</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-grid">
|
<div class="stat-grid">
|
||||||
<article class="stat-card"><strong>Timeline Steps</strong><span>${escapeHtml(run.timeline?.length || 0)}</span></article>
|
<article class="stat-card"><strong>Timeline Steps</strong><span>${escapeHtml(run.timeline?.length || 0)}</span></article>
|
||||||
<article class="stat-card"><strong>Artifacts</strong><span>${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span></article>
|
<article class="stat-card"><strong>Artifacts</strong><span>${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}</span></article>
|
||||||
<article class="stat-card"><strong>Browser</strong><span>${run.browser_evidence?.present ? "Ready" : "Missing"}</span></article>
|
<article class="stat-card"><strong>Browser</strong><span>${run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional")}</span></article>
|
||||||
<article class="stat-card"><strong>Finished</strong><span>${escapeHtml(timeAgo(run.finished_at))}</span></article>
|
<article class="stat-card"><strong>Finished</strong><span>${escapeHtml(timeAgo(run.finished_at))}</span></article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1431,12 +1710,8 @@ function renderDetail() {
|
|||||||
<details class="glass-panel accordion" open>
|
<details class="glass-panel accordion" open>
|
||||||
<summary><span>Progress Timeline</span><span class="tag">${escapeHtml(run.timeline?.length || 0)} steps</span></summary>
|
<summary><span>Progress Timeline</span><span class="tag">${escapeHtml(run.timeline?.length || 0)} steps</span></summary>
|
||||||
<div class="accordion-content">
|
<div class="accordion-content">
|
||||||
<div class="tag-row" style="margin-bottom:14px;">
|
${renderProgressStrip(run.progress)}
|
||||||
<span class="tag">completed ${escapeHtml(run.progress?.completed || 0)}</span>
|
${renderStageCards(run)}
|
||||||
<span class="tag">blocked ${escapeHtml(run.progress?.blocked || 0)}</span>
|
|
||||||
<span class="tag">skipped ${escapeHtml(run.progress?.skipped || 0)}</span>
|
|
||||||
<span class="tag">failed ${escapeHtml(run.progress?.failed || 0)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-list">
|
<div class="timeline-list">
|
||||||
${(run.timeline || []).map((item) => `
|
${(run.timeline || []).map((item) => `
|
||||||
<article class="timeline-item">
|
<article class="timeline-item">
|
||||||
@@ -1533,7 +1808,7 @@ function renderDetail() {
|
|||||||
${(advisory.secondary_source_urls || []).map((ref) => `<a href="${escapeHtml(ref)}" target="_blank" rel="noreferrer">${escapeHtml(ref)}</a>`).join("")}
|
${(advisory.secondary_source_urls || []).map((ref) => `<a href="${escapeHtml(ref)}" target="_blank" rel="noreferrer">${escapeHtml(ref)}</a>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-row" style="margin-top:16px;">
|
<div class="tag-row" style="margin-top:16px;">
|
||||||
${(advisory.secure_code_topics || []).map((topic) => `<span class="tag">${escapeHtml(topic)}</span>`).join("")}
|
${(advisory.secure_code_topics || []).map((topic) => `<a class="tag" href="./docs/secure-code-index.html" target="_blank" rel="noreferrer">${escapeHtml(topic)}</a>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -1638,6 +1913,7 @@ document.addEventListener("DOMContentLoaded", init);
|
|||||||
<button id="refreshDashboard" class="chip" type="button">Refresh Dashboard</button>
|
<button id="refreshDashboard" class="chip" type="button">Refresh Dashboard</button>
|
||||||
<label class="ghost-chip"><input id="autoRefresh" type="checkbox" checked> Auto Refresh</label>
|
<label class="ghost-chip"><input id="autoRefresh" type="checkbox" checked> Auto Refresh</label>
|
||||||
<a class="ghost-chip" href="./summary.json" target="_blank" rel="noreferrer">Open Summary JSON</a>
|
<a class="ghost-chip" href="./summary.json" target="_blank" rel="noreferrer">Open Summary JSON</a>
|
||||||
|
<a class="ghost-chip" href="./docs/project-features.html" target="_blank" rel="noreferrer">Open Feature Docs</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户