feat: add area diagnostics

This commit is contained in:
2026-06-24 16:06:18 +08:00
parent b002c44218
commit e6f26152d1
6 changed files with 314 additions and 12 deletions

View File

@@ -1031,6 +1031,7 @@ function AreaDetailPanel({
<div className="h-72 rounded-md border border-[var(--border)] p-3">
<AreaMetricTrendChart metrics={detail.monthly_metrics} />
</div>
<AreaDiagnosticsPanel diagnostics={detail.diagnostics} />
<div className="grid gap-4 lg:grid-cols-2">
<AreaMetricHistoryTable metrics={detail.monthly_metrics} />
<AreaNeighborhoodTable
@@ -1051,6 +1052,57 @@ function AreaDetailPanel({
);
}
function AreaDiagnosticsPanel({ diagnostics }: { diagnostics: AreaDetail["diagnostics"] }) {
if (!diagnostics) {
return (
<div className="rounded-md border border-[var(--border)] px-4 py-8 text-sm text-[var(--muted-foreground)]">
</div>
);
}
return (
<div className="rounded-md border border-[var(--border)] p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<p className="text-sm font-semibold text-slate-950"></p>
<Badge variant={diagnostics.anomaly_count > 0 ? "warning" : "success"}>
{diagnostics.anomaly_count}
</Badge>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
{diagnostics.metrics.map((metric) => (
<div key={metric.key} className="rounded-md border border-[var(--border)] bg-white p-3">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-slate-950">{metric.label}</span>
<Badge variant={metric.anomaly ? "warning" : "muted"}>{metric.severity}</Badge>
</div>
<p className="mt-2 text-lg font-semibold text-slate-950">
{formatMetricValue(metric.current_value, metric.unit)}
</p>
<div className="mt-3 h-2 rounded-full bg-[var(--muted)]">
<div
className={
metric.anomaly
? "h-2 rounded-full bg-[var(--warning)]"
: "h-2 rounded-full bg-[var(--primary)]"
}
style={{ width: `${Math.max(0, Math.min(100, metric.percentile))}%` }}
/>
</div>
<div className="mt-2 flex items-center justify-between text-xs text-[var(--muted-foreground)]">
<span>P{formatNumber(metric.percentile, 1)}</span>
<span>
{formatMetricValue(metric.historical_min, metric.unit)}-
{formatMetricValue(metric.historical_max, metric.unit)}
</span>
</div>
</div>
))}
</div>
</div>
);
}
function AreaMetricTrendChart({ metrics }: { metrics: AreaDetail["monthly_metrics"] }) {
const data = [...metrics].reverse();
if (data.length === 0) {

View File

@@ -332,11 +332,33 @@ export type AreaMonthlyMetric = {
raw_artifact_id: number | null;
};
export type DiagnosticMetric = {
key: string;
label: string;
unit: string;
current_value: number;
percentile: number;
historical_min: number;
historical_max: number;
sample_size: number;
anomaly: boolean;
anomaly_direction: string | null;
severity: string;
};
export type AreaDiagnostics = {
area_id: string;
month: string;
metrics: DiagnosticMetric[];
anomaly_count: number;
};
export type AreaDetail = {
profile: AreaProfile;
current_score: AreaScore | null;
score_lineage: AreaScoreLineage | null;
monthly_metrics: AreaMonthlyMetric[];
diagnostics: AreaDiagnostics | null;
neighborhood_scores: NeighborhoodScore[];
};
@@ -400,6 +422,15 @@ export async function fetchAreaDetail(areaId: string, month: string): Promise<Ar
);
}
export async function fetchAreaDiagnostics(
areaId: string,
month: string,
): Promise<AreaDiagnostics> {
return fetchJson(
`/api/v1/areas/${encodeURIComponent(areaId)}/diagnostics?month=${encodeURIComponent(month)}`,
);
}
export async function fetchNeighborhoodScores(month: string): Promise<NeighborhoodScore[]> {
return fetchJson(`/api/v1/neighborhoods/scores?month=${encodeURIComponent(month)}`);
}