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 ``; +} + +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 `
${escapeHtml(title)}
${body}
`; } function detailFindings(findings) { - if (findings.length === 0) return detailBlock("运行发现项", [["status", "无"]], "detail-block-wide"); - return `
运行发现项 -
- - - ${findings.map((item) => ` - - - - - - `).join("")} -
严重级别代码次数摘要报告
${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))}
-
+ if (!Array.isArray(findings) || findings.length === 0) { + return '
运行发现项
暂无发现项
'; + } + return `
运行发现项 · ${formatNumber(findings.length)} 条 +
+ + ${findings.map((item) => ` + + + + + + `).join("")}
严重级别代码次数摘要报告
${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))}
`; } @@ -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 - - -
-
- 调度器 - - - - -
-
- -
- config - - pvc - - analyzer - - public - - maintenance - -
- -
-
-

运行时间线

- - +
-
+

运行历史

-
-
- - - - - - -
+
+ + +
@@ -159,8 +138,8 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig - - + + @@ -168,53 +147,71 @@ export function renderWebProbeSentinelDashboardHtml(config: DashboardShellConfig -
+
+
+

运行详情

+ 未选择运行 +
+ +
+
+ +

发现分析

-
-
- - - - - - +
+ + + + + + + + +
+
-
运行 状态 场景发现项更新时间发现更新