1
0
forked from boos/BoosAPI

fix: address observability review feedback

This commit is contained in:
bnvnvnv
2026-03-11 11:59:36 +00:00
parent ce32744bad
commit a5121b8f9f
7 changed files with 47 additions and 188 deletions
@@ -315,6 +315,7 @@ describe('stats dashboard filters disabled sites', () => {
availabilityPercent: number | null;
averageLatencyMs: number | null;
buckets: Array<{
startUtc?: string;
totalRequests: number;
successCount: number;
failedCount: number;
@@ -343,5 +344,6 @@ describe('stats dashboard filters disabled sites', () => {
expect(
body.siteAvailability?.[0]?.buckets.reduce((sum, bucket) => sum + bucket.failedCount, 0),
).toBe(1);
expect(typeof body.siteAvailability?.[0]?.buckets[0]?.startUtc).toBe('string');
});
});
+3
View File
@@ -183,6 +183,7 @@ type SiteAvailabilityLogRow = {
};
type SiteAvailabilityBucketAccumulator = {
startUtc: string;
label: string;
totalRequests: number;
successCount: number;
@@ -210,6 +211,7 @@ function buildSiteAvailabilitySummaries(
Array.from({ length: SITE_AVAILABILITY_BUCKET_COUNT }, (_, index) => {
const bucketStart = new Date(startMs + index * SITE_AVAILABILITY_BUCKET_MS);
return {
startUtc: bucketStart.toISOString(),
label: formatLocalDateTime(bucketStart),
totalRequests: 0,
successCount: 0,
@@ -293,6 +295,7 @@ function buildSiteAvailabilitySummaries(
? Math.round(aggregate.latencyTotalMs / aggregate.latencyCount)
: null,
buckets: aggregate.buckets.map((bucket) => ({
startUtc: bucket.startUtc,
label: bucket.label,
totalRequests: bucket.totalRequests,
successCount: bucket.successCount,
+9 -177
View File
@@ -4200,155 +4200,6 @@ textarea {
color: var(--color-text-muted);
}
.site-observability-panel {
margin-bottom: 24px;
}
.site-observability-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.site-observability-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 4px;
}
.site-observability-subtitle {
font-size: 12px;
color: var(--color-text-muted);
}
.site-observability-legend {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--color-text-muted);
}
.site-observability-legend-text {
line-height: 1;
}
.site-observability-legend-chip {
width: 14px;
height: 14px;
border-radius: 4px;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-border) 80%, transparent);
}
.site-observability-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.site-observability-row {
padding: 12px 14px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border-light);
background: color-mix(in srgb, var(--color-bg) 88%, white);
}
.site-observability-row-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.site-observability-site-meta {
min-width: 0;
flex: 1;
}
.site-observability-site-name-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.site-observability-site-name {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.site-observability-platform-badge {
padding: 2px 8px;
border-radius: 999px;
background: color-mix(in srgb, var(--color-info-soft) 72%, white);
color: var(--color-info);
font-size: 11px;
font-weight: 600;
line-height: 1.6;
}
.site-observability-link {
font-size: 11px;
color: var(--color-info);
text-decoration: none;
}
.site-observability-link:hover {
text-decoration: underline;
}
.site-observability-site-stats {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
font-size: 12px;
color: var(--color-text-secondary);
}
.site-availability-strip {
display: grid;
grid-template-columns: repeat(24, minmax(0, 1fr));
gap: 4px;
margin-top: 10px;
}
.site-availability-cell {
width: 100%;
height: 12px;
border-radius: 4px;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-border) 78%, transparent);
}
.site-observability-empty {
padding: 28px 16px;
text-align: center;
border: 1px dashed var(--color-border);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--color-bg) 90%, white);
}
.site-observability-empty-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 4px;
}
.site-observability-empty-note {
font-size: 12px;
color: var(--color-text-muted);
}
/* ===== Responsive Adjustments ===== */
@media (max-width: 900px) {
.topbar-search-trigger {
@@ -4467,41 +4318,22 @@ textarea {
min-height: 460px;
}
.site-observability-row {
padding: 12px;
}
.site-observability-row-header {
flex-direction: column;
}
.site-observability-row-actions {
width: 100%;
justify-content: flex-end;
}
.site-observability-site-stats {
gap: 8px;
}
.site-availability-strip {
gap: 3px;
}
.site-availability-cell {
height: 10px;
border-radius: 3px;
}
.proxy-logs-filter-select,
.proxy-logs-time-field,
.proxy-logs-time-field input,
.proxy-logs-filter-reset {
width: 100%;
}
.proxy-logs-time-field {
justify-content: space-between;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
}
.proxy-logs-time-field input {
width: 100%;
min-width: 0;
}
}
+21 -6
View File
@@ -38,6 +38,7 @@ type SiteSpeedState =
| undefined;
type SiteAvailabilityBucket = {
startUtc?: string | null;
label: string;
totalRequests: number;
successCount: number;
@@ -97,7 +98,15 @@ function buildSiteLast24hLogsRoute(siteId: number): string {
return buildSiteLogsRoute(siteId, { from, to });
}
function parseAvailabilityBucketStart(label: string): Date | null {
function parseAvailabilityBucketStart(startUtc?: string | null): Date | null {
const text = (startUtc || '').trim();
if (!text) return null;
const parsed = new Date(text);
if (Number.isNaN(parsed.getTime())) return null;
return parsed;
}
function parseAvailabilityBucketLabel(label: string): Date | null {
const match = label.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(?::(\d{2}))?$/);
if (!match) return null;
const [, year, month, day, hour, minute, second = '0'] = match;
@@ -114,8 +123,14 @@ function parseAvailabilityBucketStart(label: string): Date | null {
return parsed;
}
function buildAvailabilityBucketLogsRoute(siteId: number, bucketLabel: string): string {
const start = parseAvailabilityBucketStart(bucketLabel);
function formatAvailabilityBucketLabel(bucket: SiteAvailabilityBucket): string {
const parsed = parseAvailabilityBucketStart(bucket.startUtc) || parseAvailabilityBucketLabel(bucket.label);
if (!parsed) return bucket.label;
return `${parsed.getFullYear()}-${padDateTimeSegment(parsed.getMonth() + 1)}-${padDateTimeSegment(parsed.getDate())} ${padDateTimeSegment(parsed.getHours())}:${padDateTimeSegment(parsed.getMinutes())}:${padDateTimeSegment(parsed.getSeconds())}`;
}
function buildAvailabilityBucketLogsRoute(siteId: number, bucket: SiteAvailabilityBucket): string {
const start = parseAvailabilityBucketStart(bucket.startUtc) || parseAvailabilityBucketLabel(bucket.label);
if (!start) return buildSiteLast24hLogsRoute(siteId);
const end = new Date(start.getTime() + 60 * 60 * 1000);
return buildSiteLogsRoute(siteId, { from: start, to: end });
@@ -611,21 +626,21 @@ export default function Dashboard({ adminName = '\u7ba1\u7406\u5458' }: { adminN
{site.buckets.map((bucket, index) => (
<Link
key={`${site.siteId}-${index}`}
to={buildAvailabilityBucketLogsRoute(site.siteId, bucket.label)}
to={buildAvailabilityBucketLogsRoute(site.siteId, bucket)}
className="site-availability-cell site-availability-cell-link"
style={{
background: getAvailabilityColor(bucket.availabilityPercent),
opacity: bucket.totalRequests > 0 ? 1 : 0.35,
}}
title={[
bucket.label,
formatAvailabilityBucketLabel(bucket),
bucket.totalRequests > 0
? `可用性 ${formatAvailabilityPercent(bucket.availabilityPercent)}`
: '无请求',
`${bucket.successCount} 成功 / ${bucket.failedCount} 失败`,
bucket.averageLatencyMs != null ? `平均响应 ${bucket.averageLatencyMs}ms` : '平均响应 —',
].join(' | ')}
aria-label={`${site.siteName} ${bucket.label} 使用日志`}
aria-label={`${site.siteName} ${formatAvailabilityBucketLabel(bucket)} 使用日志`}
/>
))}
</div>
@@ -276,14 +276,16 @@ describe('ProxyLogs server-driven page', () => {
});
await flushMicrotasks();
const expectedFrom = new Date(2026, 2, 9, 8, 0).toISOString();
const expectedTo = new Date(2026, 2, 9, 9, 0).toISOString();
expect(apiMock.getProxyLogs).toHaveBeenCalledWith({
limit: 50,
offset: 0,
status: 'all',
search: '',
siteId: 9,
from: '2026-03-09T08:00:00.000Z',
to: '2026-03-09T09:00:00.000Z',
from: expectedFrom,
to: expectedTo,
});
const rendered = JSON.stringify(root!.toJSON());
+7 -3
View File
@@ -1,4 +1,4 @@
import React, { useDeferredValue, useEffect, useMemo, useState, useCallback } from 'react';
import React, { useDeferredValue, useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
api,
@@ -221,6 +221,7 @@ export default function ProxyLogs() {
const [sites, setSites] = useState<Array<{ id: number; name: string; status?: string | null }>>([]);
const isMobile = useIsMobile(768);
const toast = useToast();
const loadSeq = useRef(0);
const fromApiBoundary = toApiTimeBoundary(fromInput);
const toApiBoundaryValue = toApiTimeBoundary(toInput);
const hasInvalidTimeRange = Boolean(
@@ -320,11 +321,12 @@ export default function ProxyLogs() {
}, [siteFilter, siteOptions]);
const load = useCallback(async () => {
const seq = ++loadSeq.current;
if (hasInvalidTimeRange) {
setLogs([]);
setTotal(0);
setSummary(EMPTY_SUMMARY);
setLoading(false);
if (seq === loadSeq.current) setLoading(false);
return;
}
setLoading(true);
@@ -339,13 +341,15 @@ export default function ProxyLogs() {
...(toApiBoundaryValue ? { to: toApiBoundaryValue } : {}),
};
const data = await api.getProxyLogs(params);
if (seq !== loadSeq.current) return;
setLogs(Array.isArray(data.items) ? data.items : []);
setTotal(Number(data.total || 0));
setSummary(data.summary || EMPTY_SUMMARY);
} catch (e: any) {
if (seq !== loadSeq.current) return;
toast.error(e.message || '加载日志失败');
} finally {
setLoading(false);
if (seq === loadSeq.current) setLoading(false);
}
}, [currentOffset, deferredSearchInput, fromApiBoundary, hasInvalidTimeRange, pageSize, siteFilter, statusFilter, toApiBoundaryValue, toast]);
@@ -57,6 +57,7 @@ describe('Dashboard site observability panel', () => {
availabilityPercent: 75,
averageLatencyMs: 320,
buckets: Array.from({ length: 24 }, (_, index) => ({
startUtc: new Date(Date.UTC(2026, 2, 11, index, 0, 0)).toISOString(),
label: `2026-03-11 ${String(index).padStart(2, '0')}:00:00`,
totalRequests: index < 8 ? 1 : 0,
successCount: index < 6 ? 1 : 0,