diff --git a/scripts/assets/web-probe-sentinel-dashboard/dashboard.css b/scripts/assets/web-probe-sentinel-dashboard/dashboard.css
index e4fd8bd1..be60af49 100644
--- a/scripts/assets/web-probe-sentinel-dashboard/dashboard.css
+++ b/scripts/assets/web-probe-sentinel-dashboard/dashboard.css
@@ -1,4 +1,4 @@
-/* SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery. */
+/* SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-desktop-view-density. */
/* Responsibility: Responsive visual foundation for the web-probe sentinel dashboard. */
:root {
color-scheme: light;
@@ -8,12 +8,13 @@
--text: #18212f;
--muted: #667085;
--border: #d9e0e7;
+ --border-soft: #edf0f4;
--blue: #2563eb;
--green: #059669;
--amber: #b7791f;
--red: #c2413d;
--violet: #6d5bd0;
- --shadow: 0 8px 24px rgb(30 41 59 / 8%);
+ --shadow-overlay: 0 12px 32px rgb(30 41 59 / 14%);
}
* {
@@ -42,7 +43,7 @@ input {
.sentinel-shell {
width: min(1440px, 100%);
margin: 0 auto;
- padding: 24px;
+ padding: 16px 20px;
}
.sentinel-topbar {
@@ -50,7 +51,7 @@ input {
align-items: center;
justify-content: space-between;
gap: 16px;
- margin-bottom: 18px;
+ margin-bottom: 10px;
}
.sentinel-title {
@@ -61,8 +62,8 @@ input {
}
.sentinel-mark {
- width: 36px;
- height: 36px;
+ width: 32px;
+ height: 32px;
border-radius: 8px;
background:
linear-gradient(135deg, rgb(37 99 235 / 90%), rgb(5 150 105 / 88%)),
@@ -78,48 +79,52 @@ p {
}
h1 {
- font-size: 22px;
+ font-size: 20px;
line-height: 1.18;
font-weight: 700;
}
h2 {
- font-size: 15px;
+ font-size: 14px;
line-height: 1.3;
font-weight: 700;
}
#sentinel-subtitle,
-.panel-subtitle,
-.metric-card small {
+.panel-subtitle {
color: var(--muted);
}
#sentinel-subtitle {
- margin-top: 3px;
- font-size: 13px;
+ margin-top: 2px;
+ font-size: 12px;
overflow-wrap: anywhere;
}
+#sentinel-origin-note {
+ margin-left: 6px;
+ color: #97a1b0;
+ font-size: 11px;
+}
+
.sentinel-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
- gap: 8px;
+ gap: 6px;
}
.status-pill,
.severity-pill {
display: inline-flex;
align-items: center;
- min-height: 28px;
- padding: 4px 10px;
+ min-height: 24px;
+ padding: 2px 9px;
border-radius: 999px;
border: 1px solid transparent;
font-size: 12px;
font-weight: 700;
- text-transform: uppercase;
white-space: nowrap;
}
@@ -161,10 +166,10 @@ h2 {
display: inline-flex;
align-items: center;
gap: 6px;
- min-height: 34px;
- padding: 4px 8px;
+ min-height: 32px;
+ padding: 2px 6px;
color: #344054;
- font-size: 13px;
+ font-size: 12px;
white-space: nowrap;
}
@@ -175,21 +180,21 @@ h2 {
select,
.icon-button {
- min-height: 34px;
+ min-height: 32px;
border: 1px solid var(--border);
- border-radius: 8px;
+ border-radius: 6px;
background: #ffffff;
color: #1f2937;
}
select {
- padding: 0 28px 0 10px;
+ padding: 0 24px 0 9px;
}
.icon-button {
- padding: 0 12px;
- font-size: 13px;
- font-weight: 700;
+ padding: 0 10px;
+ font-size: 12px;
+ font-weight: 600;
cursor: pointer;
}
@@ -198,10 +203,16 @@ select {
opacity: 0.62;
}
+.icon-button.active {
+ border-color: var(--blue);
+ background: #edf4ff;
+ color: var(--blue);
+}
+
.banner {
- margin-bottom: 14px;
- padding: 10px 12px;
- border-radius: 8px;
+ margin-bottom: 10px;
+ padding: 8px 12px;
+ border-radius: 6px;
border: 1px solid var(--border);
font-size: 13px;
overflow-wrap: anywhere;
@@ -219,68 +230,105 @@ select {
border-color: #f2c0bc;
}
-.metric-grid {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 12px;
- margin-bottom: 14px;
-}
-
-.metric-card,
-.panel {
- background: var(--panel);
+/* 状态摘要条:合并 4 metric cards,单行 ~48px */
+.status-summary {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px 24px;
+ margin: 0 0 10px;
+ padding: 8px 14px;
border: 1px solid var(--border);
- border-radius: 8px;
- box-shadow: var(--shadow);
+ border-radius: 6px;
+ background: var(--panel);
}
-.metric-card {
- min-height: 112px;
- padding: 14px;
- display: grid;
- align-content: space-between;
- gap: 8px;
+.summary-item {
+ display: inline-flex;
+ align-items: baseline;
+ gap: 6px;
+ min-width: 0;
}
-.metric-label {
+.summary-label {
color: var(--muted);
+ font-size: 11px;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.summary-item strong {
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 1.2;
+ overflow-wrap: anywhere;
+}
+
+.summary-item small {
+ color: var(--muted);
+ font-size: 11px;
+ overflow-wrap: anywhere;
+}
+
+.summary-checks {
+ margin-left: auto;
+}
+
+.check-summary-pill {
+ min-height: 26px;
+ padding: 3px 10px;
+ border: 1px solid #d8e0ea;
+ border-radius: 999px;
+ background: #ffffff;
+ color: #475467;
font-size: 12px;
font-weight: 700;
- text-transform: uppercase;
+ cursor: pointer;
}
-.metric-card strong {
- min-width: 0;
- font-size: 24px;
- line-height: 1.1;
- overflow-wrap: anywhere;
+.check-summary-pill.check-ok {
+ border-color: #b9e6cc;
+ background: #eefaf4;
+ color: var(--green);
}
-.metric-card small {
- font-size: 12px;
- line-height: 1.35;
- overflow-wrap: anywhere;
+.check-summary-pill.check-blocked {
+ border-color: #f4bbb7;
+ background: #fff0ee;
+ color: var(--red);
}
+/* overview checks 默认折叠为单 pill,展开看明细 */
.overview-checks {
+ margin: 0 0 10px;
+}
+
+.overview-checks-collapsed .overview-checks-detail {
+ display: none;
+}
+
+.overview-checks:not(.overview-checks-collapsed) .check-summary-pill {
+ margin-bottom: 8px;
+}
+
+.overview-checks-detail {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: 8px;
- margin: 0 0 14px;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 6px;
}
.check-chip {
display: flex;
align-items: center;
- min-height: 30px;
+ min-height: 28px;
max-width: 100%;
- padding: 5px 10px;
+ padding: 4px 9px;
border: 1px solid #d8e0ea;
- border-radius: 8px;
+ border-radius: 6px;
background: #ffffff;
color: #475467;
font-size: 12px;
- font-weight: 700;
+ font-weight: 600;
overflow-wrap: anywhere;
}
@@ -296,108 +344,100 @@ select {
color: var(--red);
}
-.timeline-panel {
- margin-bottom: 14px;
-}
-
-.run-timeline {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
- gap: 8px;
- min-height: 68px;
- padding: 14px 16px;
- overflow: visible;
-}
-
-.timeline-node {
- display: inline-grid;
- grid-template-rows: 18px auto;
- justify-items: center;
- gap: 4px;
- width: 100%;
- min-width: 0;
- max-width: none;
- border: 0;
- background: transparent;
- color: #475467;
- cursor: pointer;
-}
-
-.timeline-dot {
- width: 16px;
- height: 16px;
- border: 3px solid currentColor;
- border-radius: 999px;
- background: #ffffff;
-}
-
-.timeline-label {
- width: 100%;
- font-size: 11px;
- font-weight: 800;
- text-align: center;
- text-transform: uppercase;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
+/* 三栏 master-detail 布局:Runs | Detail | Findings */
.dashboard-grid {
display: grid;
- grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.9fr);
- align-items: start;
- gap: 14px;
+ grid-template-columns: minmax(280px, 0.85fr) minmax(0, 1.4fr) minmax(280px, 0.85fr);
+ align-items: stretch;
+ gap: 10px;
+ margin-bottom: 10px;
}
.panel {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 6px;
min-width: 0;
overflow: hidden;
+ display: flex;
+ flex-direction: column;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
- gap: 10px;
- min-height: 52px;
- padding: 14px 16px;
+ gap: 8px;
+ min-height: 40px;
+ padding: 9px 12px;
border-bottom: 1px solid var(--border);
}
-.table-frame {
- width: 100%;
- overflow: auto;
+.panel-subtitle {
+ font-size: 12px;
}
-.runs-filter,
-.findings-filter {
- display: grid;
- gap: 10px;
- padding: 12px 16px;
+.panel-runs .table-frame,
+.panel-findings .finding-list,
+.panel-detail .detail-content {
+ flex: 1 1 auto;
+}
+
+/* filter 默认折叠为单行摘要 chip */
+.filter-collapse {
border-bottom: 1px solid var(--border);
background: #fbfcfe;
}
-.runs-filter {
- grid-template-columns: repeat(4, minmax(120px, 1fr)) minmax(180px, 1.4fr) auto;
+.filter-summary {
+ display: block;
+ width: 100%;
+ padding: 8px 12px;
+ border: 0;
+ background: transparent;
+ color: #475467;
+ font-size: 12px;
+ font-weight: 600;
+ text-align: left;
+ cursor: pointer;
}
+.filter-summary::before {
+ content: "▸";
+ margin-right: 6px;
+ color: var(--muted);
+}
+
+.filter-summary[aria-expanded="true"]::before {
+ content: "▾";
+}
+
+.runs-filter,
.findings-filter {
- grid-template-columns: repeat(2, minmax(96px, 0.9fr)) repeat(2, minmax(120px, 1.1fr)) auto;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ gap: 8px;
+ padding: 10px 12px;
}
.runs-filter label,
.findings-filter label {
display: grid;
- gap: 4px;
+ gap: 3px;
min-width: 0;
+ flex: 1 1 110px;
+}
+
+.runs-filter .filter-search {
+ flex: 1 1 160px;
}
.runs-filter label span,
.findings-filter label span {
color: var(--muted);
font-size: 11px;
- font-weight: 800;
- text-transform: uppercase;
+ font-weight: 600;
}
.runs-filter input,
@@ -405,48 +445,51 @@ select {
.findings-filter input,
.findings-filter select {
width: 100%;
-}
-
-.runs-filter input,
-.findings-filter input {
- min-height: 34px;
- padding: 0 10px;
+ min-height: 30px;
+ padding: 0 8px;
border: 1px solid var(--border);
- border-radius: 8px;
+ border-radius: 6px;
background: #ffffff;
color: #1f2937;
+ font-size: 12px;
}
.runs-filter .icon-button,
.findings-filter .icon-button {
+ min-height: 30px;
align-self: end;
}
+.table-frame {
+ width: 100%;
+ overflow: auto;
+}
+
.runs-table {
width: 100%;
- min-width: 760px;
+ min-width: 420px;
border-collapse: collapse;
}
.runs-table th,
.runs-table td {
- padding: 11px 12px;
- border-bottom: 1px solid #edf0f4;
+ padding: 7px 10px;
+ border-bottom: 1px solid var(--border-soft);
text-align: left;
vertical-align: top;
- font-size: 13px;
+ font-size: 12px;
}
.runs-table th {
color: var(--muted);
background: #f8fafc;
font-size: 11px;
- font-weight: 800;
- text-transform: uppercase;
+ font-weight: 700;
}
.runs-table tr {
cursor: pointer;
+ border-left: 3px solid transparent;
}
.runs-table tr:hover {
@@ -455,10 +498,35 @@ select {
.runs-table tr.selected-row {
background: #eef7ff;
+ border-left-color: var(--blue);
+}
+
+/* 行级 severity 色条 */
+.runs-table tr.severity-red,
+.runs-table tr.severity-critical,
+.runs-table tr.severity-error {
+ border-left-color: var(--red);
+}
+
+.runs-table tr.severity-warning,
+.runs-table tr.severity-amber {
+ border-left-color: var(--amber);
+}
+
+.runs-table tr.severity-info {
+ border-left-color: #97a1b0;
+}
+
+.runs-table tr.selected-row.severity-red,
+.runs-table tr.selected-row.severity-critical,
+.runs-table tr.selected-row.severity-error {
+ border-left-color: var(--red);
+ background: #fdeff0;
}
.runs-table tr.limit-row {
cursor: default;
+ border-left-color: transparent;
}
.runs-table tr.limit-row:hover {
@@ -468,13 +536,12 @@ select {
.runs-table tr.limit-row td {
color: var(--muted);
font-size: 12px;
- font-weight: 700;
text-align: center;
}
.runs-table td small {
display: block;
- margin-top: 3px;
+ margin-top: 2px;
color: var(--muted);
font-size: 11px;
overflow-wrap: anywhere;
@@ -482,28 +549,20 @@ select {
.run-identity {
display: grid;
- gap: 5px;
+ gap: 2px;
min-width: 0;
}
-.run-identity div {
- display: grid;
- grid-template-columns: 62px minmax(0, 1fr);
- align-items: baseline;
- gap: 8px;
-}
-
-.run-identity span {
- color: var(--muted);
- font-size: 10px;
- font-weight: 800;
- text-transform: uppercase;
-}
-
.run-identity code {
min-width: 0;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
- font-size: 12px;
+ font-size: 11px;
+ overflow-wrap: anywhere;
+}
+
+.run-identity small {
+ color: var(--muted);
+ font-size: 10px;
overflow-wrap: anywhere;
}
@@ -513,63 +572,310 @@ select {
overflow-wrap: anywhere;
}
-.finding-list {
- display: grid;
- gap: 12px;
- padding: 12px;
+/* Detail tab */
+.detail-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2px;
+ padding: 6px 8px 0;
+ border-bottom: 1px solid var(--border);
+ background: #fbfcfe;
}
-.finding-group {
- min-width: 0;
- border: 1px solid #e3e8ef;
- border-radius: 8px;
- background: #ffffff;
-}
-
-.finding-group summary {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 4px 10px;
- align-items: center;
- min-height: 46px;
- padding: 10px 12px;
+.detail-tab {
+ min-height: 30px;
+ padding: 4px 12px;
+ border: 1px solid transparent;
+ border-bottom: 0;
+ border-radius: 6px 6px 0 0;
+ background: transparent;
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 600;
cursor: pointer;
}
-.finding-group summary span {
- font-size: 13px;
- font-weight: 800;
+.detail-tab:hover {
+ background: #ffffff;
+ color: #344054;
}
-.finding-group summary strong {
- font-size: 20px;
- line-height: 1;
+.detail-tab.active {
+ background: var(--panel);
+ border-color: var(--border);
+ color: var(--blue);
}
-.finding-group summary small {
- grid-column: 1 / -1;
+.detail-content {
+ padding: 12px 14px 14px;
+ overflow: auto;
+}
+
+.detail-block {
+ min-width: 0;
+ margin-bottom: 14px;
+ padding: 0 0 0 10px;
+ border-left: 2px solid #dbe5ee;
+}
+
+.detail-block:last-child {
+ margin-bottom: 0;
+}
+
+.detail-block strong {
+ display: block;
+ margin-bottom: 6px;
+ font-size: 12px;
+ color: #344054;
+}
+
+.detail-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 4px 16px;
+}
+
+.detail-grid > div {
+ display: grid;
+ grid-template-columns: 92px minmax(0, 1fr);
+ gap: 8px;
+ align-items: baseline;
+}
+
+.detail-grid span {
color: var(--muted);
+ font-size: 11px;
+}
+
+.detail-grid code,
+.detail-grid em {
+ font-style: normal;
+ font-size: 12px;
+ overflow-wrap: anywhere;
+}
+
+.detail-table-frame {
+ width: 100%;
+ overflow: auto;
+}
+
+.detail-table {
+ width: 100%;
+ min-width: 520px;
+ border-collapse: collapse;
+}
+
+.detail-table th,
+.detail-table td {
+ padding: 6px 8px;
+ border-bottom: 1px solid var(--border-soft);
+ text-align: left;
+ vertical-align: top;
font-size: 12px;
}
-.finding-group-list {
- display: grid;
- gap: 10px;
- padding: 0 10px 10px;
+.detail-table th {
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 700;
}
+.command-list {
+ display: grid;
+ gap: 8px;
+}
+
+.command-row {
+ display: grid;
+ grid-template-columns: minmax(80px, 0.3fr) minmax(0, 1fr) auto;
+ gap: 8px;
+ align-items: center;
+}
+
+.command-row .command-label {
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.copy-button {
+ min-height: 28px;
+ padding: 3px 9px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: #ffffff;
+ color: #344054;
+ font-size: 11px;
+ font-weight: 600;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.copy-button:hover {
+ border-color: var(--blue);
+ color: var(--blue);
+}
+
+.copy-button.copied {
+ border-color: var(--green);
+ color: var(--green);
+}
+
+.command-code {
+ display: block;
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid #e3e8ef;
+ border-radius: 6px;
+ background: #f8fafc;
+ color: #1f2937;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ font-size: 11px;
+ line-height: 1.4;
+ overflow-wrap: anywhere;
+ white-space: pre-wrap;
+}
+
+.view-note {
+ color: var(--muted);
+ font-size: 11px;
+ margin-bottom: 6px;
+}
+
+.detail-pre {
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ margin: 6px 0 0;
+ padding: 9px;
+ border: 1px solid #e3e8ef;
+ border-radius: 6px;
+ background: #f8fafc;
+ color: #1f2937;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ font-size: 12px;
+ line-height: 1.45;
+ overflow: auto;
+ white-space: pre-wrap;
+}
+
+.detail-pre {
+ min-height: 120px;
+ max-height: 380px;
+}
+
+.trace-reader-grid {
+ display: grid;
+ grid-template-columns: minmax(200px, 0.7fr) minmax(0, 1.3fr);
+ gap: 10px;
+}
+
+.trace-turn-picker,
+.trace-frame-view {
+ min-width: 0;
+}
+
+.trace-choice-list {
+ display: grid;
+ gap: 6px;
+ margin-top: 6px;
+ max-height: 360px;
+ overflow: auto;
+}
+
+.trace-choice {
+ display: grid;
+ gap: 3px;
+ width: 100%;
+ padding: 7px 9px;
+ border: 1px solid #d8e0ea;
+ border-radius: 6px;
+ background: #ffffff;
+ color: #344054;
+ cursor: pointer;
+ text-align: left;
+}
+
+.trace-choice.active {
+ border-color: #95c7ff;
+ background: #edf6ff;
+}
+
+.trace-choice span {
+ min-width: 0;
+ font-size: 12px;
+ font-weight: 600;
+ overflow-wrap: anywhere;
+}
+
+.trace-choice small {
+ color: var(--muted);
+ font-size: 11px;
+ overflow-wrap: anywhere;
+}
+
+.trace-frame-pre {
+ min-height: 280px;
+ max-height: 520px;
+}
+
+.trace-source-note {
+ margin-top: 8px;
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.final-response-block {
+ margin-top: 10px;
+ padding: 9px;
+ border: 1px solid #b9e6cc;
+ border-radius: 6px;
+ background: #eefaf4;
+}
+
+.final-response-block.empty,
+.final-response-block.unavailable {
+ border-color: #d8e0ea;
+ background: #f8fafc;
+}
+
+.final-response-block strong {
+ display: block;
+ margin-bottom: 4px;
+ color: #1f2937;
+ font-size: 12px;
+}
+
+.final-response-text {
+ min-height: 56px;
+ max-height: 240px;
+ margin: 6px 0 0;
+ padding: 8px;
+ border: 1px solid #d8e0ea;
+ border-radius: 6px;
+ background: #ffffff;
+ color: #1f2937;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ font-size: 12px;
+ line-height: 1.45;
+ overflow: auto;
+ white-space: pre-wrap;
+}
+
+/* Findings:聚合优先,drill-down 到列表 */
.finding-aggregation {
display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 10px;
- padding: 12px;
+ grid-template-columns: 1fr;
+ gap: 8px;
+ padding: 10px 12px;
border-bottom: 1px solid var(--border);
background: #ffffff;
}
.aggregation-group {
- display: grid;
- align-content: start;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
gap: 6px;
min-width: 0;
}
@@ -577,35 +883,105 @@ select {
.aggregation-group > span {
color: var(--muted);
font-size: 11px;
- font-weight: 800;
- text-transform: uppercase;
+ font-weight: 600;
+ min-width: 50px;
}
-.finding-item {
+.aggregation-group .finding-meta {
+ font-size: 12px;
+}
+
+.finding-list {
display: grid;
gap: 8px;
- padding: 12px;
+ padding: 10px;
+}
+
+.findings-drilldown-back {
+ margin-bottom: 8px;
+ padding: 0;
+}
+
+.finding-group {
+ min-width: 0;
border: 1px solid #e3e8ef;
- border-radius: 8px;
+ border-radius: 6px;
+ background: #ffffff;
+}
+
+.finding-group summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ min-height: 36px;
+ padding: 7px 10px;
+ cursor: pointer;
+}
+
+.finding-group summary span {
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.finding-group summary strong {
+ font-size: 16px;
+ line-height: 1;
+}
+
+.finding-group summary small {
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.finding-group-list {
+ display: grid;
+ gap: 8px;
+ padding: 0 8px 8px;
+}
+
+/* finding item 默认折叠,只显示 title + severity + count */
+.finding-item {
+ display: grid;
+ gap: 6px;
+ padding: 9px 10px;
+ border: 1px solid #e3e8ef;
+ border-radius: 6px;
background: var(--panel-soft);
}
+.finding-item.is-collapsed .finding-detail {
+ display: none;
+}
+
.finding-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
+ cursor: pointer;
}
.finding-title {
min-width: 0;
- font-weight: 700;
+ font-size: 12px;
+ font-weight: 600;
overflow-wrap: anywhere;
}
+.finding-title small {
+ margin-left: 6px;
+ color: var(--muted);
+}
+
+.finding-detail {
+ display: grid;
+ gap: 6px;
+}
+
.finding-summary {
color: #1f2937;
- font-size: 13px;
+ font-size: 12px;
line-height: 1.45;
overflow-wrap: anywhere;
}
@@ -624,14 +1000,14 @@ select {
background: #ffffff;
color: #344054;
cursor: pointer;
- font-size: 12px;
- font-weight: 700;
+ font-size: 11px;
+ font-weight: 600;
line-height: 1.3;
}
.filter-chip {
- min-height: 28px;
- padding: 4px 9px;
+ min-height: 24px;
+ padding: 2px 8px;
overflow-wrap: anywhere;
text-align: left;
}
@@ -644,8 +1020,8 @@ select {
.link-button {
justify-self: start;
- min-height: 30px;
- padding: 5px 10px;
+ min-height: 26px;
+ padding: 3px 9px;
}
.link-button:disabled {
@@ -653,216 +1029,97 @@ select {
opacity: 0.6;
}
-.finding-meta,
-.detail-grid {
- color: var(--muted);
- font-size: 12px;
- line-height: 1.45;
-}
-
-.detail-panel {
- margin-top: 14px;
-}
-
-.detail-content {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 12px;
- padding: 14px 16px 16px;
-}
-
-.detail-block {
- min-width: 0;
- padding: 2px 0 2px 12px;
- border-left: 3px solid #dbe5ee;
-}
-
-.detail-block-wide {
- grid-column: span 2;
-}
-
-.detail-block strong {
- display: block;
- margin-bottom: 8px;
- font-size: 12px;
- text-transform: uppercase;
- color: var(--muted);
-}
-
-.detail-table-frame {
- width: 100%;
- overflow: auto;
-}
-
-.detail-table {
- width: 100%;
- min-width: 720px;
- border-collapse: collapse;
-}
-
-.detail-table th,
-.detail-table td {
- padding: 8px 9px;
- border-bottom: 1px solid #edf0f4;
- text-align: left;
- vertical-align: top;
- font-size: 12px;
-}
-
-.detail-table th {
- color: var(--muted);
- font-size: 10px;
- font-weight: 800;
- text-transform: uppercase;
-}
-
-.command-list {
- display: grid;
- gap: 8px;
-}
-
-.command-list div {
- display: grid;
- gap: 4px;
-}
-
-.command-list span,
-.view-note {
+.finding-meta {
color: var(--muted);
font-size: 11px;
- font-weight: 800;
- text-transform: uppercase;
-}
-
-.command-list code,
-.detail-pre {
- display: block;
- width: 100%;
- max-width: 100%;
- border: 1px solid #e3e8ef;
- border-radius: 8px;
- background: #f8fafc;
- color: #1f2937;
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
- font-size: 12px;
line-height: 1.45;
- overflow: auto;
}
-.command-list code {
- padding: 8px 9px;
- white-space: pre-wrap;
- overflow-wrap: anywhere;
+/* Timeline 底部紧凑条 */
+.timeline-panel {
+ margin-bottom: 0;
}
-.detail-pre {
- min-height: 160px;
- max-height: 420px;
- margin: 8px 0 0;
- padding: 10px;
- white-space: pre-wrap;
-}
-
-.trace-reader-grid {
- display: grid;
- grid-template-columns: minmax(240px, 0.72fr) minmax(0, 1.28fr);
- gap: 12px;
-}
-
-.trace-turn-picker,
-.trace-frame-view {
- min-width: 0;
-}
-
-.trace-choice-list {
- display: grid;
- gap: 8px;
- margin-top: 8px;
- max-height: 420px;
- overflow: auto;
-}
-
-.trace-choice {
- display: grid;
- gap: 4px;
- width: 100%;
- padding: 9px 10px;
- border: 1px solid #d8e0ea;
- border-radius: 8px;
+.timeline-toggle {
+ min-height: 26px;
+ padding: 2px 9px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
background: #ffffff;
color: #344054;
- cursor: pointer;
- text-align: left;
-}
-
-.trace-choice.active {
- border-color: #95c7ff;
- background: #edf6ff;
-}
-
-.trace-choice span {
- min-width: 0;
- font-size: 12px;
- font-weight: 800;
- overflow-wrap: anywhere;
-}
-
-.trace-choice small {
- color: var(--muted);
font-size: 11px;
- overflow-wrap: anywhere;
+ font-weight: 600;
+ cursor: pointer;
}
-.trace-frame-pre {
- min-height: 360px;
- max-height: 620px;
+.timeline-panel.collapsed .run-timeline {
+ display: none;
}
-.trace-source-note {
- margin-top: 8px;
- color: var(--muted);
+.run-timeline {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px;
+ min-height: 40px;
+ padding: 10px 12px;
+ overflow: visible;
}
-.final-response-block {
- margin-top: 12px;
- padding: 10px;
- border: 1px solid #b9e6cc;
- border-radius: 8px;
- background: #eefaf4;
+.timeline-node {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ width: auto;
+ min-width: 0;
+ max-width: none;
+ border: 0;
+ background: transparent;
+ color: #475467;
+ cursor: pointer;
}
-.final-response-block.empty,
-.final-response-block.unavailable {
- border-color: #d8e0ea;
- background: #f8fafc;
-}
-
-.final-response-block strong {
- display: block;
- margin-bottom: 6px;
- color: #1f2937;
- font-size: 12px;
- text-transform: uppercase;
-}
-
-.final-response-text {
- min-height: 64px;
- max-height: 260px;
- margin: 8px 0 0;
- padding: 9px;
- border: 1px solid #d8e0ea;
- border-radius: 8px;
+.timeline-node::before {
+ content: "";
+ width: 8px;
+ height: 8px;
+ border: 2px solid currentColor;
+ border-radius: 999px;
background: #ffffff;
- color: #1f2937;
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ flex: 0 0 auto;
+}
+
+.timeline-node + .timeline-node::before {
+ margin-left: 2px;
+}
+
+.timeline-label {
+ font-size: 11px;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.timeline-age {
+ color: var(--muted);
+ font-size: 10px;
+}
+
+.copy-toast {
+ position: fixed;
+ bottom: 16px;
+ right: 16px;
+ padding: 8px 14px;
+ border-radius: 6px;
+ background: #18212f;
+ color: #ffffff;
font-size: 12px;
- line-height: 1.45;
- overflow: auto;
- white-space: pre-wrap;
+ font-weight: 600;
+ box-shadow: var(--shadow-overlay);
+ z-index: 100;
}
.empty-state {
- padding: 28px 16px;
+ padding: 20px 14px;
color: var(--muted);
text-align: center;
font-size: 13px;
@@ -877,9 +1134,26 @@ select {
display: none !important;
}
+/* 桌面端 ≥1024px:三栏;移动端保持原有 @media 断点行为 */
+@media (max-width: 1240px) {
+ .dashboard-grid {
+ grid-template-columns: minmax(260px, 1fr) minmax(0, 1.6fr) minmax(260px, 1fr);
+ }
+}
+
+@media (max-width: 1024px) {
+ .dashboard-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .status-summary {
+ gap: 4px 16px;
+ }
+}
+
@media (max-width: 980px) {
.sentinel-shell {
- padding: 16px;
+ padding: 12px 14px;
}
.sentinel-topbar {
@@ -892,45 +1166,42 @@ select {
width: 100%;
}
- .metric-grid,
- .dashboard-grid,
- .detail-content {
- grid-template-columns: 1fr;
+ .summary-checks {
+ margin-left: 0;
}
.runs-filter {
- grid-template-columns: repeat(2, minmax(0, 1fr));
+ flex-wrap: wrap;
}
- .findings-filter,
- .finding-aggregation {
+ .findings-filter {
+ flex-wrap: wrap;
+ }
+
+ .detail-grid {
grid-template-columns: 1fr;
}
- .detail-block-wide {
- grid-column: span 1;
- }
-
.trace-reader-grid {
grid-template-columns: 1fr;
}
- .metric-card {
- min-height: 96px;
+ .command-row {
+ grid-template-columns: 1fr auto;
}
- .run-timeline {
- grid-template-columns: repeat(3, minmax(0, 1fr));
+ .command-row .command-label {
+ grid-column: 1 / -1;
}
}
@media (max-width: 560px) {
.sentinel-shell {
- padding: 12px;
+ padding: 10px;
}
h1 {
- font-size: 19px;
+ font-size: 18px;
}
.sentinel-toolbar > * {
@@ -950,12 +1221,15 @@ select {
flex-direction: column;
}
- .runs-filter {
- grid-template-columns: 1fr;
+ .runs-filter,
+ .findings-filter {
+ flex-direction: column;
+ align-items: stretch;
}
- .findings-filter {
- grid-template-columns: 1fr;
+ .runs-filter label,
+ .findings-filter label {
+ flex: 1 1 auto;
}
.table-frame {
@@ -980,19 +1254,19 @@ select {
}
.runs-table tr {
- padding: 10px 12px;
- border-bottom: 1px solid #edf0f4;
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border-soft);
}
.runs-table tr.limit-row {
- padding: 12px;
+ padding: 10px;
}
.runs-table td {
display: grid;
- grid-template-columns: 82px minmax(0, 1fr);
- gap: 8px;
- padding: 6px 0;
+ grid-template-columns: 70px minmax(0, 1fr);
+ gap: 6px;
+ padding: 4px 0;
border-bottom: 0;
}
@@ -1000,8 +1274,7 @@ select {
content: attr(data-label);
color: var(--muted);
font-size: 10px;
- font-weight: 800;
- text-transform: uppercase;
+ font-weight: 700;
}
.runs-table tr.limit-row td {
@@ -1014,15 +1287,7 @@ select {
content: none;
}
- .run-identity div {
- grid-template-columns: 54px minmax(0, 1fr);
- }
-
- .run-timeline {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
.finding-group:not([open]) summary {
- min-height: 42px;
+ min-height: 34px;
}
}
diff --git a/scripts/assets/web-probe-sentinel-dashboard/dashboard.js b/scripts/assets/web-probe-sentinel-dashboard/dashboard.js
index 54b7411d..7c18df55 100644
--- a/scripts/assets/web-probe-sentinel-dashboard/dashboard.js
+++ b/scripts/assets/web-probe-sentinel-dashboard/dashboard.js
@@ -1,5 +1,7 @@
-// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery.
+// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-desktop-view-density.
// Responsibility: Browser-side API client, formatting, auto refresh, and base dashboard rendering.
+// Desktop view redesign (issue #1025): 三栏 master-detail、低噪声、渐进披露、排查直觉化。
+// 保持纯 vanilla JS + 原生 CSS,无框架、无构建步骤,所有渲染通过 innerHTML 拼接。
/**
* @typedef {{runId?: string|null, observerId?: string|null, stateDir?: string|null, reportJsonSha256?: string|null, stateRoot?: string|null, source?: string|null}} Traceability
@@ -20,14 +22,20 @@ const refs = {
manualRefresh: document.getElementById("manual-refresh"),
autoRefreshEnabled: document.getElementById("auto-refresh-enabled"),
autoRefreshInterval: document.getElementById("auto-refresh-interval"),
- overall: document.getElementById("metric-overall"),
- origin: document.getElementById("metric-origin"),
- latestRun: document.getElementById("metric-latest-run"),
- latestAge: document.getElementById("metric-latest-age"),
- findings: document.getElementById("metric-findings"),
- findingsNote: document.getElementById("metric-findings-note"),
- scheduler: document.getElementById("metric-scheduler"),
- budget: document.getElementById("metric-budget"),
+ latestRun: document.getElementById("latest-run"),
+ filterRed: document.getElementById("filter-red"),
+ originNote: document.getElementById("sentinel-origin-note"),
+ statusSummary: document.getElementById("status-summary"),
+ summaryStatus: document.getElementById("summary-status"),
+ summaryLatest: document.getElementById("summary-latest"),
+ summaryLatestAge: document.getElementById("summary-latest-age"),
+ summaryFindings: document.getElementById("summary-findings"),
+ summaryFindingsNote: document.getElementById("summary-findings-note"),
+ summaryScheduler: document.getElementById("summary-scheduler"),
+ summaryBudget: document.getElementById("summary-budget"),
+ summaryChecks: document.getElementById("summary-checks"),
+ overviewChecks: document.getElementById("overview-checks"),
+ checkSummaryPill: document.getElementById("check-summary-pill"),
checkConfig: document.getElementById("check-config"),
checkPvc: document.getElementById("check-pvc"),
checkAnalyzer: document.getElementById("check-analyzer"),
@@ -35,6 +43,8 @@ const refs = {
checkMaintenance: document.getElementById("check-maintenance"),
timeline: document.getElementById("run-timeline"),
timelineCount: document.getElementById("timeline-count"),
+ timelineToggle: document.getElementById("timeline-toggle"),
+ runsFilterSummary: document.getElementById("runs-filter-summary"),
filterForm: document.getElementById("runs-filter"),
filterStatus: document.getElementById("filter-status"),
filterSeverity: document.getElementById("filter-severity"),
@@ -44,6 +54,7 @@ const refs = {
clearFilters: document.getElementById("clear-filters"),
runsBody: document.getElementById("runs-body"),
runsCount: document.getElementById("runs-count"),
+ findingsFilterSummary: document.getElementById("findings-filter-summary"),
findingsFilterForm: document.getElementById("findings-filter"),
findingFilterSeverity: document.getElementById("finding-filter-severity"),
findingFilterWindow: document.getElementById("finding-filter-window"),
@@ -53,8 +64,11 @@ const refs = {
findingAggregation: document.getElementById("finding-aggregation"),
findingsList: document.getElementById("findings-list"),
findingsCount: document.getElementById("findings-count"),
+ findingsDrilldown: document.getElementById("findings-drilldown"),
+ detailTabs: document.getElementById("detail-tabs"),
detailSubtitle: document.getElementById("detail-subtitle"),
detailContent: document.getElementById("detail-content"),
+ copyToast: document.getElementById("copy-toast"),
};
const state = {
@@ -73,11 +87,21 @@ const state = {
lastUpdatedAt: null,
filters: readFiltersFromLocation(),
findingFilters: readFindingFiltersFromLocation(),
+ selectedTab: readTabFromLocation(),
+ selectedFindingCode: new URLSearchParams(window.location.search).get("finding") || "",
+ findingDrilldownKey: "",
+ runsFilterOpen: false,
+ findingsFilterOpen: false,
+ checksExpanded: false,
+ timelineCollapsed: false,
+ pausedByInteraction: false,
};
+const DETAIL_TABS = ["overview", "findings", "turn", "trace", "evidence"];
+
const dashboardLimits = {
- timeline: { mobile: 6, tablet: 9, desktop: 12 },
- runs: { mobile: 8, tablet: 12, desktop: 24 },
+ timeline: { mobile: 6, tablet: 9, desktop: 16 },
+ runs: { mobile: 8, tablet: 12, desktop: 30 },
findingsRed: { mobile: 4, tablet: 6, desktop: 8 },
findingsOther: { mobile: 2, tablet: 3, desktop: 5 },
};
@@ -88,12 +112,18 @@ const autoRefresh = createAutoRefresh({
intervals: [5, 10, 30],
defaultInterval: 10,
onRefresh: () => loadDashboard({ silent: true }),
- shouldPause: () => document.hidden || state.loading,
+ shouldPause: () => document.hidden || state.loading || state.pausedByInteraction,
});
refs.manualRefresh.addEventListener("click", () => loadDashboard({ silent: false }));
refs.autoRefreshEnabled.addEventListener("change", () => autoRefresh.setEnabled(refs.autoRefreshEnabled.checked));
refs.autoRefreshInterval.addEventListener("change", () => autoRefresh.setInterval(Number(refs.autoRefreshInterval.value)));
+refs.latestRun.addEventListener("click", () => jumpToLatestRun());
+refs.filterRed.addEventListener("click", () => toggleRedOnly());
+refs.runsFilterSummary.addEventListener("click", () => toggleRunsFilter());
+refs.findingsFilterSummary.addEventListener("click", () => toggleFindingsFilter());
+refs.checkSummaryPill.addEventListener("click", () => toggleChecks());
+refs.timelineToggle.addEventListener("click", () => toggleTimeline());
refs.filterForm.addEventListener("submit", (event) => event.preventDefault());
for (const control of [refs.filterStatus, refs.filterSeverity, refs.filterWindow, refs.filterSort]) {
control.addEventListener("change", () => applyFilterControls());
@@ -102,6 +132,7 @@ refs.filterSearch.addEventListener("input", debounce(() => applyFilterControls()
refs.clearFilters.addEventListener("click", () => {
state.filters = { status: "", severity: "", window: "", search: "", sort: "updated" };
writeFiltersToControls();
+ updateRunsFilterSummary();
syncLocationQuery();
loadDashboard({ silent: false });
});
@@ -114,23 +145,34 @@ for (const control of [refs.findingFilterCode, refs.findingFilterScenario]) {
}
refs.findingClearFilters.addEventListener("click", () => {
state.findingFilters = { severity: "", window: "24h", code: "", scenario: "" };
+ state.findingDrilldownKey = "";
writeFindingFiltersToControls();
+ updateFindingsFilterSummary();
syncLocationQuery();
loadDashboard({ silent: false });
});
+refs.detailTabs.addEventListener("click", (event) => {
+ const button = event.target.closest("[data-detail-tab]");
+ if (button) selectTab(button.dataset.detailTab);
+});
document.addEventListener("visibilitychange", () => {
if (!document.hidden && autoRefresh.enabled()) loadDashboard({ silent: true });
});
-window.addEventListener("resize", debounce(() => {
- if (!state.overview) return;
- renderTimeline();
- renderRuns();
- renderFindings();
-}, 150));
+document.addEventListener("keydown", handleKeyboardShortcut);
+window.addEventListener("resize", debounce(updateViewportClass, 150));
+
+// 交互暂停:hover runs 表格或在 detail tab 阅读时暂停 auto-refresh
+refs.runsBody.addEventListener("mouseenter", () => { state.pausedByInteraction = true; }, true);
+refs.runsBody.addEventListener("mouseleave", () => { state.pausedByInteraction = false; }, true);
+refs.detailContent.addEventListener("mouseenter", () => { state.pausedByInteraction = true; }, true);
+refs.detailContent.addEventListener("mouseleave", () => { state.pausedByInteraction = false; }, true);
hydrateControls();
writeFiltersToControls();
writeFindingFiltersToControls();
+updateRunsFilterSummary();
+updateFindingsFilterSummary();
+updateViewportClass();
loadDashboard({ silent: false }).catch((error) => renderError(error));
function createDashboardApi() {
@@ -271,6 +313,7 @@ async function selectRun(runId) {
syncLocationQuery();
renderRuns();
refs.detailSubtitle.textContent = runId;
+ refs.detailTabs.hidden = false;
refs.detailContent.innerHTML = '
加载中
';
try {
const [detail, views] = await Promise.all([
@@ -309,45 +352,68 @@ function renderOverview() {
const latestStatus = latest?.status || status;
refs.statusPill.textContent = displayStatus(latestStatus);
refs.statusPill.className = `status-pill ${statusClass(latestStatus)}`;
- refs.overall.textContent = displayStatus(latestStatus);
- refs.origin.textContent = `${overview.publicOrigin || root.dataset.publicOrigin || "-"} · 历史 ${displayStatus(status)}`;
- refs.latestRun.textContent = latest?.runId || latest?.id || "-";
- refs.latestAge.textContent = latest?.updatedAt ? `${formatRelative(latest.updatedAt)} 更新` : "-";
+ // 状态摘要条合并 status-pill 信息(B1 去重:不再单独显示 metric-overall 重复值)
+ refs.statusSummary.hidden = false;
+ refs.summaryStatus.textContent = displayStatus(latestStatus);
+ refs.summaryStatus.className = statusClass(latestStatus);
+ refs.originNote.textContent = overview.publicOrigin || root.dataset.publicOrigin || "";
+
+ refs.summaryLatest.textContent = latest?.runId || latest?.id || "-";
+ refs.summaryLatestAge.textContent = latest?.updatedAt ? `${formatRelative(latest.updatedAt)} 更新` : "-";
const severityCounts = overview.severityCounts || {};
const totalFindings = Object.values(severityCounts).reduce((sum, value) => sum + Number(value || 0), 0);
- refs.findings.textContent = formatNumber(totalFindings);
- refs.findingsNote.textContent = formatSeveritySummary(severityCounts);
+ refs.summaryFindings.textContent = formatNumber(totalFindings);
+ refs.summaryFindingsNote.textContent = formatSeveritySummary(severityCounts);
const scheduler = overview.scheduler || {};
const heartbeat = scheduler.heartbeat || {};
- refs.scheduler.textContent = heartbeat.at ? formatRelative(heartbeat.at) : "未知";
+ refs.summaryScheduler.textContent = heartbeat.at ? formatRelative(heartbeat.at) : "未知";
const maxSeconds = overview.targetValidation?.maxSeconds ?? 120;
- refs.budget.textContent = `targetValidation 预算 ${maxSeconds}s`;
+ refs.summaryBudget.textContent = `预算 ${maxSeconds}s`;
const checks = overview.health?.checks || {};
+ const checkResults = [
+ { label: "配置", ok: checks.config?.ok, detail: checks.config?.status },
+ { label: "PVC", ok: checks.pvc?.ok, detail: checks.pvc?.stateRoot },
+ { label: "分析器", ok: checks.analyzer?.ok, detail: "observe analyze" },
+ { label: "公开入口", ok: Boolean(overview.publicOrigin), detail: overview.publicOrigin || "-" },
+ ];
+ const maintenance = overview.maintenance || {};
+ checkResults.push({ label: "维护窗口", ok: maintenance.active !== true, detail: maintenance.active ? "生效中" : "未生效" });
+
+ const allOk = checkResults.every((item) => item.ok);
+ const okCount = checkResults.filter((item) => item.ok).length;
+ refs.summaryChecks.hidden = false;
+ refs.checkSummaryPill.textContent = allOk ? `✓ ${okCount}/${checkResults.length} 检查通过` : `${okCount}/${checkResults.length} 检查`;
+ refs.checkSummaryPill.className = `check-summary-pill ${allOk ? "check-ok" : "check-blocked"}`;
+ refs.overviewChecks.hidden = false;
+
renderCheckChip(refs.checkConfig, "配置", checks.config?.ok, checks.config?.status);
renderCheckChip(refs.checkPvc, "PVC", checks.pvc?.ok, checks.pvc?.stateRoot);
renderCheckChip(refs.checkAnalyzer, "分析器", checks.analyzer?.ok, "observe analyze");
renderCheckChip(refs.checkPublic, "公开入口", Boolean(overview.publicOrigin), overview.publicOrigin || "-");
- const maintenance = overview.maintenance || {};
renderCheckChip(refs.checkMaintenance, "维护窗口", maintenance.active !== true, maintenance.active ? "生效中" : "未生效");
+
+ // B3: 全绿时默认折叠 overview checks 明细
+ refs.overviewChecks.classList.toggle("overview-checks-collapsed", !state.checksExpanded);
}
function renderTimeline() {
const visibleRuns = state.runs.slice(0, responsiveLimit(dashboardLimits.timeline));
- refs.timelineCount.textContent = `最近 ${formatNumber(visibleRuns.length)} / ${formatNumber(state.runs.length)} 次`;
+ refs.timelineCount.textContent = `最近 ${formatNumber(visibleRuns.length)} / ${formatNumber(state.runs.length)}`;
if (state.runs.length === 0) {
- refs.timeline.innerHTML = '暂无时间线
';
+ refs.timeline.innerHTML = '暂无时间线
';
return;
}
refs.timeline.innerHTML = visibleRuns.map((run) => {
const runId = run.runId || run.id || "-";
- const title = `${displayStatus(run.status)} · ${run.findingCount ?? 0} 个发现 · ${run.updatedAt ? formatRelative(run.updatedAt) : "-"}`;
+ const age = run.updatedAt ? formatRelative(run.updatedAt) : "-";
+ const title = `${displayStatus(run.status)} · ${run.findingCount ?? 0} 个发现 · ${age}`;
return ``;
}).join("");
for (const node of refs.timeline.querySelectorAll("[data-run-id]")) {
@@ -359,7 +425,7 @@ function renderRuns() {
const visibleLimit = responsiveLimit(dashboardLimits.runs);
const visibleRuns = state.runs.slice(0, visibleLimit);
const hiddenCount = Math.max(0, state.runs.length - visibleRuns.length);
- refs.runsCount.textContent = `${formatNumber(visibleRuns.length)} / ${formatNumber(state.runs.length)} 条可见`;
+ refs.runsCount.textContent = `${formatNumber(visibleRuns.length)} / ${formatNumber(state.runs.length)} 条`;
if (state.runs.length === 0) {
refs.runsBody.innerHTML = '| 暂无运行 |
';
return;
@@ -367,17 +433,15 @@ function renderRuns() {
refs.runsBody.innerHTML = visibleRuns.map((run) => {
const runId = run.runId || run.id || "-";
const selected = state.selectedRunId === runId ? " selected-row" : "";
- return `
-
- run${escapeHtml(runId)}
- observer${escapeHtml(run.observerId || "-")}
- |
+ const severityRowClass = run.maxSeverity ? ` ${severityClass(run.maxSeverity)}` : "";
+ return `
+ ${escapeHtml(shortText(runId, 24))}${escapeHtml(run.observerId ? shortText(run.observerId, 28) : "-")}
|
${escapeHtml(displayStatus(run.status))} |
${escapeHtml(run.scenarioId || "-")} |
- ${escapeHtml(String(run.findingCount ?? 0))}${run.maxSeverity ? ` ${escapeHtml(displaySeverity(run.maxSeverity))}` : ""} |
- ${escapeHtml(run.updatedAt ? formatRelative(run.updatedAt) : "-")}${escapeHtml(run.maintenance ? "维护窗口" : "")} |
+ ${escapeHtml(String(run.findingCount ?? 0))}${run.maxSeverity ? ` ${escapeHtml(displaySeverity(run.maxSeverity))}` : ""} |
+ ${escapeHtml(run.updatedAt ? formatRelative(run.updatedAt) : "-")}${run.maintenance ? `维护窗口` : ""} |
`;
- }).join("") + (hiddenCount > 0 ? `| 其余 ${formatNumber(hiddenCount)} 条运行通过筛选、搜索或 API drill-down 查看 |
` : "");
+ }).join("") + (hiddenCount > 0 ? `| 其余 ${formatNumber(hiddenCount)} 条通过筛选、搜索或 API drill-down 查看 |
` : "");
for (const row of refs.runsBody.querySelectorAll("tr[data-run-id]")) {
row.addEventListener("click", () => selectRun(row.dataset.runId));
}
@@ -386,28 +450,99 @@ function renderRuns() {
function renderFindings() {
renderFindingAggregation();
refs.findingsCount.textContent = `${formatNumber(state.findings.length)} 组`;
+ if (state.findingDrilldownKey) {
+ renderFindingsDrilldown();
+ return;
+ }
+ refs.findingsList.hidden = false;
+ refs.findingsDrilldown.hidden = true;
if (state.findings.length === 0) {
refs.findingsList.innerHTML = '暂无发现项
';
return;
}
+ // B8: 默认只显示聚合,list 折叠为 severity group summary(点击展开明细)
const groups = groupedFindingsBySeverity(state.findings);
- refs.findingsList.innerHTML = groups.map((group, groupIndex) => {
- const open = group.key === "red" || groupIndex === 0;
+ refs.findingsList.innerHTML = groups.map((group) => {
const visibleItems = group.items.slice(0, findingVisibleLimit(group.key));
const hiddenCount = Math.max(0, group.items.length - visibleItems.length);
- return `
+ return `
${escapeHtml(displaySeverity(group.key))}
${formatNumber(group.totalCount)}
- ${formatNumber(group.items.length)} 组${hiddenCount > 0 ? ` · 其余 ${formatNumber(hiddenCount)} 组通过过滤查看` : ""}
+ ${formatNumber(group.items.length)} 组${hiddenCount > 0 ? ` · 其余 ${formatNumber(hiddenCount)} 组` : ""}
- ${visibleItems.map(renderFindingItem).join("")}
+ ${visibleItems.map((item) => renderFindingItemCollapsed(item)).join("")}
`;
}).join("");
- for (const button of refs.findingsList.querySelectorAll("[data-open-finding-run]")) {
+ bindFindingItemEvents();
+}
+
+function renderFindingsDrilldown() {
+ refs.findingsList.hidden = true;
+ refs.findingsDrilldown.hidden = false;
+ const [filterKey, filterValue] = state.findingDrilldownKey.split(":", 2);
+ const matched = state.findings.filter((item) => {
+ if (filterKey === "severity") return String(item.severity || "unknown").toLowerCase() === filterValue;
+ if (filterKey === "code") return (item.code || item.findingId || "unknown") === filterValue;
+ if (filterKey === "scenario") return (item.scenarioId || "unknown") === filterValue;
+ return false;
+ });
+ const label = drilldownLabel(filterKey, filterValue);
+ refs.findingsDrilldown.innerHTML = `
+ drill-down: ${escapeHtml(label)} · ${formatNumber(matched.length)} 组
+ ${matched.map((item) => renderFindingItemCollapsed(item)).join("")}
`;
+ document.getElementById("findings-drilldown-back")?.addEventListener("click", () => {
+ state.findingDrilldownKey = "";
+ syncLocationQuery();
+ renderFindings();
+ });
+ bindFindingItemEvents(refs.findingsDrilldown);
+}
+
+function drilldownLabel(filterKey, filterValue) {
+ if (filterKey === "severity") return `严重级别=${displaySeverity(filterValue)}`;
+ if (filterKey === "code") return `代码=${displayFindingCode(filterValue)}`;
+ if (filterKey === "scenario") return `场景=${filterValue}`;
+ return filterValue;
+}
+
+function renderFindingItemCollapsed(item) {
+ const code = item.code || item.findingId || "finding";
+ const latestRunId = item.latestRunId || "-";
+ const hasLatestRun = latestRunId !== "-";
+ const codeLabel = displayFindingCode(code);
+ return `
+
+ ${escapeHtml(codeLabel)}${codeLabel === code ? "" : ` ${escapeHtml(code)}`} · ${formatNumber(item.count ?? 0)} 次
+ ${escapeHtml(displaySeverity(item.severity))}
+
+
+
${escapeHtml(shortText(displayFindingSummary(code, item.summary || ""), 220))}
+
+
+
+
+
次数=${escapeHtml(String(item.count ?? 0))} · 运行=${escapeHtml(String(item.runCount ?? 0))} · 最近=${escapeHtml(item.latestAt ? formatRelative(item.latestAt) : "-")}
+
run=${escapeHtml(latestRunId)} report=${escapeHtml(item.latestReportJsonSha256 || "-")}
+
${escapeHtml(findingNextAction(code))}
+
+
+ `;
+}
+
+function bindFindingItemEvents(scope) {
+ const container = scope || refs.findingsList;
+ for (const row of container.querySelectorAll("[data-finding-toggle]")) {
+ row.addEventListener("click", (event) => {
+ if (event.target.closest("[data-open-finding-run]") || event.target.closest("[data-finding-filter]")) return;
+ const item = row.closest(".finding-item");
+ if (item) item.classList.toggle("is-collapsed");
+ });
+ }
+ for (const button of container.querySelectorAll("[data-open-finding-run]")) {
button.addEventListener("click", () => selectRun(button.dataset.openFindingRun));
}
- for (const button of refs.findingsList.querySelectorAll("[data-finding-filter]")) {
+ for (const button of container.querySelectorAll("[data-finding-filter]")) {
button.addEventListener("click", () => applyFindingFilter(button.dataset.findingFilter, button.dataset.filterValue || ""));
}
}
@@ -423,28 +558,6 @@ function findingVisibleLimit(severity) {
return responsiveLimit(key === "red" || key === "critical" || key === "error" ? dashboardLimits.findingsRed : dashboardLimits.findingsOther);
}
-function renderFindingItem(item) {
- const code = item.code || item.findingId || "finding";
- const latestRunId = item.latestRunId || "-";
- const hasLatestRun = latestRunId !== "-";
- const codeLabel = displayFindingCode(code);
- return `
-
- ${escapeHtml(codeLabel)}${codeLabel === code ? "" : ` ${escapeHtml(code)}`}
- ${escapeHtml(displaySeverity(item.severity))}
-
- ${escapeHtml(shortText(displayFindingSummary(code, item.summary || ""), 180))}
-
-
-
-
- 次数=${escapeHtml(String(item.count ?? 0))} · 运行=${escapeHtml(String(item.runCount ?? 0))} · 最近=${escapeHtml(item.latestAt ? formatRelative(item.latestAt) : "-")}
- run=${escapeHtml(latestRunId)} report=${escapeHtml(item.latestReportJsonSha256 || "-")}
- ${escapeHtml(findingNextAction(code))}
-
- `;
-}
-
function groupedFindingsBySeverity(findings) {
const severityOrder = ["red", "critical", "error", "warning", "amber", "info", "unknown"];
const groups = new Map();
@@ -482,7 +595,7 @@ function renderFindingAggregation() {
`窗口
`,
].join("");
for (const button of refs.findingAggregation.querySelectorAll("[data-finding-filter]")) {
- button.addEventListener("click", () => applyFindingFilter(button.dataset.findingFilter, button.dataset.filterValue || ""));
+ button.addEventListener("click", () => openFindingsDrilldown(button.dataset.findingFilter, button.dataset.filterValue || ""));
}
}
@@ -504,22 +617,45 @@ function aggregateFindings(keyFn) {
.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
}
+function openFindingsDrilldown(filterKey, filterValue) {
+ if (!filterValue || filterValue === "-") return;
+ state.findingDrilldownKey = `${filterKey}:${filterValue}`;
+ syncLocationQuery();
+ renderFindings();
+}
+
function applyFindingFilter(key, value) {
if (!value || value === "-") return;
if (key === "severity") state.findingFilters.severity = value;
if (key === "code") state.findingFilters.code = value;
if (key === "scenario") state.findingFilters.scenario = value;
writeFindingFiltersToControls();
+ updateFindingsFilterSummary();
syncLocationQuery();
loadDashboard({ silent: false });
}
+function selectTab(tab) {
+ if (!DETAIL_TABS.includes(tab)) return;
+ state.selectedTab = tab;
+ for (const button of refs.detailTabs.querySelectorAll("[data-detail-tab]")) {
+ button.classList.toggle("active", button.dataset.detailTab === tab);
+ }
+ syncLocationQuery();
+ renderDetail();
+}
+
function renderDetail() {
if (!state.selectedRunId || !state.runDetail) {
refs.detailSubtitle.textContent = "未选择运行";
+ refs.detailTabs.hidden = true;
refs.detailContent.innerHTML = '请选择一条运行
';
return;
}
+ const tab = state.selectedTab || "overview";
+ for (const button of refs.detailTabs.querySelectorAll("[data-detail-tab]")) {
+ button.classList.toggle("active", button.dataset.detailTab === tab);
+ }
const detail = state.runDetail;
const run = detail.run || {};
const findings = Array.isArray(detail.findings) ? detail.findings : [];
@@ -527,7 +663,34 @@ function renderDetail() {
const commands = detail.commands || {};
const turnSummaryView = selectedView(state.runViews, "turn-summary");
refs.detailSubtitle.textContent = run.runId || state.selectedRunId;
- refs.detailContent.innerHTML = [
+ refs.detailContent.innerHTML = renderDetailTab(tab, detail, run, findings, artifacts, commands, turnSummaryView);
+ bindDetailControls();
+}
+
+function renderDetailTab(tab, detail, run, findings, artifacts, commands, turnSummaryView) {
+ if (tab === "findings") {
+ return detailFindings(findings);
+ }
+ if (tab === "turn") {
+ return detailTurnSummary(turnSummaryView);
+ }
+ if (tab === "trace") {
+ return detailTraceReader(state.runViews, commands);
+ }
+ if (tab === "evidence") {
+ return [
+ detailArtifacts(artifacts),
+ detailCommands(commands),
+ detailEvidence(detail, artifacts),
+ detailBlock("脱敏", [
+ ["values", detail.valuesRedacted === true ? "已脱敏" : "-"],
+ ["prompt", detail.redaction?.prompt || "-"],
+ ["assistant", detail.redaction?.assistantFinal || "-"],
+ ]),
+ ].join("");
+ }
+ // overview
+ return [
detailBlock("追溯信息", [
["run", run.runId || "-"],
["observer", run.observerId || "-"],
@@ -542,44 +705,29 @@ function renderDetail() {
["updated", run.updatedAt || "-"],
["views", Array.isArray(detail.viewsAvailable) ? detail.viewsAvailable.join(", ") : "-"],
]),
- detailBlock("报告摘要", safeSummaryRows(detail.summary), "detail-block-wide"),
- detailFindings(findings),
- detailArtifacts(artifacts),
- detailCommands(commands),
- detailTurnSummary(turnSummaryView),
- detailTraceReader(state.runViews, commands),
- detailEvidence(detail, artifacts),
- detailBlock("脱敏", [
- ["values", detail.valuesRedacted === true ? "已脱敏" : "-"],
- ["prompt", detail.redaction?.prompt || "-"],
- ["assistant", detail.redaction?.assistantFinal || "-"],
- ]),
+ detailBlock("报告摘要", safeSummaryRows(detail.summary)),
].join("");
- bindDetailControls();
}
-function detailBlock(title, rows, className = "") {
- const visibleRows = rows.length > 0 ? rows : [["-", "-"]];
- return `${escapeHtml(title)}${
- visibleRows.map(([key, value]) => `
${escapeHtml(key)}: ${escapeHtml(shortText(value, 180))}
`).join("")
- }
`;
+function detailBlock(title, rows, extraClass) {
+ const body = rows.map(([key, value]) => `${escapeHtml(key)}${escapeHtml(String(value ?? "-"))}
`).join("");
+ return ``;
}
function detailFindings(findings) {
- if (findings.length === 0) return detailBlock("运行发现项", [["status", "无"]], "detail-block-wide");
- return `运行发现项
-
-
- | 严重级别 | 代码 | 次数 | 摘要 | 报告 |
- ${findings.map((item) => `
- | ${escapeHtml(displaySeverity(item.severity))} |
- ${escapeHtml(item.finding_id || item.findingId || "-")} |
- ${escapeHtml(String(item.count ?? 0))} |
- ${escapeHtml(shortText(displayFindingSummary(item.finding_id || item.findingId || "", item.summary || ""), 220))} |
- ${escapeHtml(shortText(item.report_json_sha256 || item.reportJsonSha256 || "-", 24))} |
-
`).join("")}
-
-
+ if (!Array.isArray(findings) || findings.length === 0) {
+ return '运行发现项暂无发现项
';
+ }
+ return `运行发现项 · ${formatNumber(findings.length)} 条
+
+ | 严重级别 | 代码 | 次数 | 摘要 | 报告 |
+
${findings.map((item) => `
+ | ${escapeHtml(displaySeverity(item.severity))} |
+ ${escapeHtml(item.finding_id || item.findingId || "-")} |
+ ${escapeHtml(String(item.count ?? 0))} |
+ ${escapeHtml(shortText(displayFindingSummary(item.finding_id || item.findingId || "", item.summary || ""), 220))} |
+ ${escapeHtml(shortText(item.report_json_sha256 || item.reportJsonSha256 || "-", 24))} |
+
`).join("")}
`;
}
@@ -591,7 +739,7 @@ function detailArtifacts(artifacts) {
["screenshotPath", screenshot.path || "-"],
["screenshotSha256", screenshot.sha256 || "-"],
["publicOrigin", artifacts.publicOrigin || "-"],
- ], "detail-block-wide");
+ ]);
}
function detailCommands(commands) {
@@ -600,17 +748,22 @@ function detailCommands(commands) {
["turn-summary", commands.turnSummary || "-"],
["trace-frame", commands.traceFrame || "-"],
];
- return `CLI 对照命令
- ${rows.map(([label, command]) => `
${escapeHtml(label)}${escapeHtml(command)}
`).join("")}
+ // D4: CLI 命令改可复制按钮
+ return `CLI 对照命令
+ ${rows.map(([label, command]) => `
+ ${escapeHtml(label)}
+ ${escapeHtml(command)}
+
+
`).join("")}
`;
}
function detailTurnSummary(view) {
- if (!view) return detailBlock("多轮摘要 - 第一层", [["status", "未索引"]], "detail-block-wide");
- if (view.ok === false) return detailBlock("多轮摘要 - 第一层", [["status", view.error || "不可用"]], "detail-block-wide");
+ if (!view) return detailBlock("多轮摘要 - 第一层", [["status", "未索引"]]);
+ if (view.ok === false) return detailBlock("多轮摘要 - 第一层", [["status", view.error || "不可用"]]);
const text = redactDisplayText(view.renderedText || "");
const note = `${formatNumber(view.renderedTextBytes || text.length)} bytes${view.truncated ? " 已截断" : ""}`;
- return `多轮摘要 - 第一层
+ return `多轮摘要 - 第一层
${escapeHtml(note)}
${escapeHtml(text || "-")}
`;
@@ -626,7 +779,7 @@ function detailTraceReader(response, commands) {
const traceNote = traceView
? `${formatNumber(traceView.renderedTextBytes || traceText.length)} bytes${traceView.truncated ? " 已截断" : ""}`
: "trace-frame 未索引";
- return `Trace Frame - 第二层
+ return `Trace Frame - 第二层
选择 turn / trace / sample
@@ -692,7 +845,7 @@ function detailEvidence(detail, artifacts) {
["observerId", traceability.observerId || "-"],
["runId", traceability.runId || "-"],
["reportJsonSha256", traceability.reportJsonSha256 || artifacts.reportJsonSha256 || "-"],
- ], "detail-block-wide");
+ ]);
}
function selectedView(response, viewName) {
@@ -708,6 +861,38 @@ function bindDetailControls() {
renderDetail();
});
}
+ for (const button of refs.detailContent.querySelectorAll("[data-copy-command]")) {
+ button.addEventListener("click", () => copyCommand(button));
+ }
+}
+
+async function copyCommand(button) {
+ const command = button.dataset.copyCommand || "";
+ try {
+ await navigator.clipboard.writeText(command);
+ } catch {
+ // 降级:用临时 textarea
+ const textarea = document.createElement("textarea");
+ textarea.value = command;
+ textarea.style.position = "fixed";
+ textarea.style.opacity = "0";
+ document.body.appendChild(textarea);
+ textarea.select();
+ try { document.execCommand("copy"); } catch { /* ignore */ }
+ document.body.removeChild(textarea);
+ }
+ button.classList.add("copied");
+ button.textContent = "已复制";
+ showCopyToast();
+ window.setTimeout(() => {
+ button.classList.remove("copied");
+ button.textContent = "复制";
+ }, 1500);
+}
+
+function showCopyToast() {
+ refs.copyToast.hidden = false;
+ window.setTimeout(() => { refs.copyToast.hidden = true; }, 1200);
}
function renderLoading(show) {
@@ -721,6 +906,110 @@ function renderCheckChip(element, label, ok, detail) {
element.className = `check-chip ${ok ? "check-ok" : "check-blocked"}`;
}
+function toggleChecks() {
+ state.checksExpanded = !state.checksExpanded;
+ refs.overviewChecks.classList.toggle("overview-checks-collapsed", !state.checksExpanded);
+}
+
+function toggleTimeline() {
+ state.timelineCollapsed = !state.timelineCollapsed;
+ const panel = refs.timeline.closest(".timeline-panel");
+ panel.classList.toggle("collapsed", state.timelineCollapsed);
+ refs.timelineToggle.textContent = state.timelineCollapsed ? "展开" : "折叠";
+ refs.timelineToggle.setAttribute("aria-expanded", String(!state.timelineCollapsed));
+}
+
+function toggleRunsFilter() {
+ state.runsFilterOpen = !state.runsFilterOpen;
+ refs.filterForm.hidden = !state.runsFilterOpen;
+ refs.runsFilterSummary.setAttribute("aria-expanded", String(state.runsFilterOpen));
+ updateRunsFilterSummary();
+}
+
+function toggleFindingsFilter() {
+ state.findingsFilterOpen = !state.findingsFilterOpen;
+ refs.findingsFilterForm.hidden = !state.findingsFilterOpen;
+ refs.findingsFilterSummary.setAttribute("aria-expanded", String(state.findingsFilterOpen));
+ updateFindingsFilterSummary();
+}
+
+function updateRunsFilterSummary() {
+ const f = state.filters;
+ const parts = [];
+ if (f.status) parts.push(`状态=${displayStatus(f.status)}`);
+ if (f.severity) parts.push(`严重=${displaySeverity(f.severity)}`);
+ if (f.window) parts.push(`时间=${f.window}`);
+ if (f.search) parts.push(`搜索="${shortText(f.search, 16)}"`);
+ if (f.sort && f.sort !== "updated") parts.push(`排序=${f.sort}`);
+ refs.runsFilterSummary.textContent = `筛选: ${parts.length ? parts.join(" · ") : "全部"}`;
+}
+
+function updateFindingsFilterSummary() {
+ const f = state.findingFilters;
+ const parts = [];
+ if (f.severity) parts.push(`严重=${displaySeverity(f.severity)}`);
+ parts.push(`窗口=${f.window || "全部"}`);
+ if (f.code) parts.push(`代码="${shortText(f.code, 16)}"`);
+ if (f.scenario) parts.push(`场景="${shortText(f.scenario, 16)}"`);
+ refs.findingsFilterSummary.textContent = `筛选: ${parts.join(" · ")}`;
+}
+
+function jumpToLatestRun() {
+ const latest = state.runs[0];
+ if (latest) selectRun(latest.runId || latest.id);
+}
+
+function toggleRedOnly() {
+ const active = refs.filterRed.classList.toggle("active");
+ if (active) {
+ state.filters.severity = "red";
+ } else {
+ state.filters.severity = "";
+ }
+ refs.filterSeverity.value = state.filters.severity;
+ updateRunsFilterSummary();
+ syncLocationQuery();
+ loadDashboard({ silent: false });
+}
+
+function handleKeyboardShortcut(event) {
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement || event.target instanceof HTMLTextAreaElement) return;
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
+ const key = event.key.toLowerCase();
+ if (key === "r") { event.preventDefault(); loadDashboard({ silent: false }); return; }
+ if (key === "/") { event.preventDefault(); state.runsFilterOpen = true; refs.filterForm.hidden = false; refs.runsFilterSummary.setAttribute("aria-expanded", "true"); refs.filterSearch.focus(); return; }
+ if (key === "j" || key === "k") { event.preventDefault(); navigateRun(key === "j" ? 1 : -1); return; }
+ if (key === "enter") {
+ event.preventDefault();
+ if (state.selectedRunId) { selectTab("overview"); return; }
+ const first = state.runs[0];
+ if (first) selectRun(first.runId || first.id);
+ return;
+ }
+}
+
+function navigateRun(direction) {
+ const ids = state.runs.map((run) => run.runId || run.id).filter(Boolean);
+ if (ids.length === 0) return;
+ const currentIndex = state.selectedRunId ? ids.indexOf(state.selectedRunId) : -1;
+ let nextIndex = currentIndex + direction;
+ if (nextIndex < 0) nextIndex = 0;
+ if (nextIndex >= ids.length) nextIndex = ids.length - 1;
+ const nextId = ids[nextIndex];
+ if (nextId && nextId !== state.selectedRunId) selectRun(nextId);
+}
+
+function updateViewportClass() {
+ const width = window.innerWidth;
+ const viewport = width >= 1024 ? "desktop" : width >= 981 ? "tablet" : "mobile";
+ root.dataset.viewport = viewport;
+ if (state.overview) {
+ renderTimeline();
+ renderRuns();
+ renderFindings();
+ }
+}
+
function applyFilterControls() {
state.filters = {
status: refs.filterStatus.value,
@@ -729,6 +1018,8 @@ function applyFilterControls() {
search: refs.filterSearch.value.trim(),
sort: refs.filterSort.value || "updated",
};
+ updateRunsFilterSummary();
+ refs.filterRed.classList.toggle("active", state.filters.severity === "red");
syncLocationQuery();
loadDashboard({ silent: false });
}
@@ -740,6 +1031,7 @@ function applyFindingFilterControls() {
code: refs.findingFilterCode.value.trim(),
scenario: refs.findingFilterScenario.value.trim(),
};
+ updateFindingsFilterSummary();
syncLocationQuery();
loadDashboard({ silent: false });
}
@@ -750,11 +1042,12 @@ function writeFiltersToControls() {
refs.filterWindow.value = state.filters.window || "";
refs.filterSearch.value = state.filters.search || "";
refs.filterSort.value = state.filters.sort || "updated";
+ refs.filterRed.classList.toggle("active", state.filters.severity === "red");
}
function writeFindingFiltersToControls() {
refs.findingFilterSeverity.value = state.findingFilters.severity || "";
- refs.findingFilterWindow.value = state.findingFilters.window || "";
+ refs.findingFilterWindow.value = state.findingFilters.window || "24h";
refs.findingFilterCode.value = state.findingFilters.code || "";
refs.findingFilterScenario.value = state.findingFilters.scenario || "";
}
@@ -780,6 +1073,11 @@ function readFindingFiltersFromLocation() {
};
}
+function readTabFromLocation() {
+ const tab = new URLSearchParams(window.location.search).get("tab");
+ return DETAIL_TABS.includes(tab) ? tab : "overview";
+}
+
function syncLocationQuery() {
const query = new URLSearchParams();
for (const [key, value] of Object.entries(state.filters)) {
@@ -795,6 +1093,9 @@ function syncLocationQuery() {
if (value && !(key === "window" && value === "24h")) query.set(findingQueryKeys[key], value);
}
if (state.selectedRunId) query.set("run", state.selectedRunId);
+ // C3: URL 支持 tab deep-link
+ if (state.selectedRunId && state.selectedTab && state.selectedTab !== "overview") query.set("tab", state.selectedTab);
+ if (state.findingDrilldownKey) query.set("finding", state.findingDrilldownKey);
const next = `${window.location.pathname}${query.toString() ? `?${query.toString()}` : ""}`;
window.history.replaceState(null, "", next);
}
@@ -1003,9 +1304,9 @@ function formatRelative(iso) {
if (seconds < 5) return "刚刚";
if (seconds < 60) return `${seconds} 秒前`;
const minutes = Math.floor(seconds / 60);
- if (minutes < 60) return `${minutes} 分钟前`;
+ if (minutes < 60) return `${minutes} 分前`;
const hours = Math.floor(minutes / 60);
- if (hours < 24) return `${hours} 小时前`;
+ if (hours < 24) return `${hours} 时前`;
return `${Math.floor(hours / 24)} 天前`;
}
diff --git a/scripts/src/hwlab-node-web-sentinel-dashboard-assets.ts b/scripts/src/hwlab-node-web-sentinel-dashboard-assets.ts
index f7a39141..5c038836 100644
--- a/scripts/src/hwlab-node-web-sentinel-dashboard-assets.ts
+++ b/scripts/src/hwlab-node-web-sentinel-dashboard-assets.ts
@@ -1,4 +1,4 @@
-// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p8-web-probe-sentinel-recovery.
+// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-desktop-view-density.
// Responsibility: Static dashboard shell and asset serving for the web-probe sentinel frontend.
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
@@ -11,7 +11,7 @@ interface DashboardShellConfig {
}
const DASHBOARD_ASSET_ROOT = "scripts/assets/web-probe-sentinel-dashboard";
-const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p8-web-probe-sentinel-recovery";
+const DASHBOARD_CONTRACT_VERSION = "draft-2026-06-26-p9-desktop-view-density";
export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig): string {
const publicOrigin = stringOrNull(config.publicExposure.publicBaseUrl) ?? "";
@@ -32,126 +32,105 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig
data-public-origin="${escapeAttr(publicOrigin)}"
data-config-ready="${config.plan.ok ? "true" : "false"}"
data-contract-version="${DASHBOARD_CONTRACT_VERSION}"
+ data-viewport=""
>
HWLAB Web哨兵
-
${escapeHtml(config.node)} / ${escapeHtml(config.lane)}
+
${escapeHtml(config.node)} / ${escapeHtml(config.lane)}
空闲
-
+
+
+
-
+
+ 状态-
+ 最近运行-
+ 发现0
+ 调度器-
+
+
+
+
-
-
- 当前状态
- -
- -
-
-
- 最近运行
- -
- -
-
-
- 历史发现项
- 0
- -
-
-
- 调度器
- -
- -
-
-
-
-
- config -
- pvc -
- analyzer -
- public -
- maintenance -
-
-
-