|
96 | 96 | <div class="stats shadow bg-base-100"> |
97 | 97 | <div class="stat py-3 px-4"> |
98 | 98 | <div class="stat-title text-xs">Dangerous</div> |
99 | | - <div class="stat-value text-lg text-error">{{ report.summary?.dangerous ?? 0 }}</div> |
| 99 | + <div class="stat-value text-lg text-error">{{ threatCounts.dangerous }}</div> |
100 | 100 | </div> |
101 | 101 | </div> |
102 | 102 | <div class="stats shadow bg-base-100"> |
103 | 103 | <div class="stat py-3 px-4"> |
104 | 104 | <div class="stat-title text-xs">Warnings</div> |
105 | | - <div class="stat-value text-lg text-warning">{{ report.summary?.warnings ?? 0 }}</div> |
| 105 | + <div class="stat-value text-lg text-warning">{{ threatCounts.warnings }}</div> |
106 | 106 | </div> |
107 | 107 | </div> |
108 | 108 | <div class="stats shadow bg-base-100"> |
109 | 109 | <div class="stat py-3 px-4"> |
110 | 110 | <div class="stat-title text-xs">Info</div> |
111 | | - <div class="stat-value text-lg text-info">{{ report.summary?.info_level ?? 0 }}</div> |
| 111 | + <div class="stat-value text-lg text-info">{{ threatCounts.info }}</div> |
112 | 112 | </div> |
113 | 113 | </div> |
114 | 114 | <div class="stats shadow bg-base-100"> |
115 | 115 | <div class="stat py-3 px-4"> |
116 | 116 | <div class="stat-title text-xs">Total</div> |
117 | | - <div class="stat-value text-lg">{{ report.summary?.total ?? 0 }}</div> |
| 117 | + <div class="stat-value text-lg">{{ threatCounts.total }}</div> |
118 | 118 | </div> |
119 | 119 | </div> |
120 | 120 | </div> |
|
555 | 555 | </div> |
556 | 556 | <div class="flex gap-2"> |
557 | 557 | <button |
558 | | - v-if="serverAdminState === 'enabled' && report.summary?.dangerous > 0" |
| 558 | + v-if="serverAdminState === 'enabled' && reportStatus === 'dangerous'" |
559 | 559 | @click="quarantineServer" |
560 | 560 | :disabled="actionLoading" |
561 | 561 | class="btn btn-error btn-sm" |
@@ -638,17 +638,41 @@ const scanContext = computed(() => { |
638 | 638 | return report.value?.scan_context || null |
639 | 639 | }) |
640 | 640 |
|
641 | | -// Status display |
| 641 | +// Status display. Spec 077 FR-014 verdict purity: the badge shows the |
| 642 | +// tier-driven, baseline-only `verdict` computed server-side with the SAME |
| 643 | +// predicate as the server-list status, so this page can never say "dangerous" |
| 644 | +// while the server list says "clean" (a tierless deep-scan/external finding |
| 645 | +// never moves the verdict). Raw summary counts remain only as a fallback for |
| 646 | +// reports served by a core that predates the verdict field. |
642 | 647 | const reportStatus = computed(() => { |
643 | 648 | if (!report.value) return 'unknown' |
644 | 649 | if (report.value.scan_complete === false) return 'incomplete' |
645 | 650 | if (report.value.empty_scan) return 'empty' |
646 | 651 | if (!report.value.findings || report.value.findings.length === 0) return 'clean' |
| 652 | + if (report.value.verdict) return report.value.verdict |
647 | 653 | if (report.value.summary?.dangerous > 0) return 'dangerous' |
648 | 654 | if (report.value.summary?.warnings > 0) return 'warnings' |
649 | 655 | return 'clean' |
650 | 656 | }) |
651 | 657 |
|
| 658 | +// Threat tiles use the tier-driven buckets (finding_counts) matching the |
| 659 | +// server list exactly (Spec 077 FR-014): a tierless deep-scan "dangerous" |
| 660 | +// finding counts as a warning on BOTH surfaces. Falls back to the raw |
| 661 | +// threat-level summary for pre-Spec-077 payloads. |
| 662 | +const threatCounts = computed(() => { |
| 663 | + const r = report.value |
| 664 | + const fc = r?.finding_counts |
| 665 | + if (fc) { |
| 666 | + return { dangerous: fc.dangerous ?? 0, warnings: fc.warning ?? 0, info: fc.info ?? 0, total: fc.total ?? 0 } |
| 667 | + } |
| 668 | + return { |
| 669 | + dangerous: r?.summary?.dangerous ?? 0, |
| 670 | + warnings: r?.summary?.warnings ?? 0, |
| 671 | + info: r?.summary?.info_level ?? 0, |
| 672 | + total: r?.summary?.total ?? 0, |
| 673 | + } |
| 674 | +}) |
| 675 | +
|
652 | 676 | const statusBadgeClass = computed(() => { |
653 | 677 | switch (reportStatus.value) { |
654 | 678 | case 'dangerous': return 'badge-error' |
@@ -800,8 +824,15 @@ async function quarantineServer() { |
800 | 824 |
|
801 | 825 | // F-04: Go through the security-aware approval path instead of the legacy |
802 | 826 | // unquarantine endpoint. hasUnresolvedCritical disables the primary Approve |
803 | | -// button so the user must use Force Approve explicitly. |
| 827 | +// button so the user must use Force Approve explicitly. It mirrors the |
| 828 | +// backend approval gate (Spec 077 FR-021: hard-tier BASELINE findings only) |
| 829 | +// via the tier-driven finding_counts — a tierless deep-scan/external finding |
| 830 | +// or a non-blocking soft finding with "critical" severity must not lock the |
| 831 | +// Approve button when the backend would accept. Raw summary.critical is only |
| 832 | +// a fallback for payloads that predate finding_counts. |
804 | 833 | const hasUnresolvedCritical = computed(() => { |
| 834 | + const fc = report.value?.finding_counts |
| 835 | + if (fc) return (fc.dangerous ?? 0) > 0 |
805 | 836 | return (report.value?.summary?.critical ?? 0) > 0 |
806 | 837 | }) |
807 | 838 |
|
|
0 commit comments