More bugfixes
All checks were successful
CI / test (push) Successful in 53s
CI / test-analytics (push) Successful in 1m59s
CI / build-api (push) Successful in 40s
CI / build-frontend (push) Successful in 1m55s
CI / build-analytics (push) Successful in 2m46s

This commit is contained in:
2026-06-27 15:53:32 +02:00
parent 980233840e
commit cf54ccc7b3
5 changed files with 39 additions and 26 deletions

View File

@@ -68,14 +68,14 @@ export const getWWIJobs = (limit = 50) =>
get<JobExecution[]>(`/api/jobs/wwi?limit=${limit}`, "frontend.jobs.wwi");
export const triggerAWJob = (jobName: string) =>
post<{ triggered: boolean; job_name: string }>(
`/api/jobs/aw/${jobName}/trigger`,
post<{ status: string; job: string }>(
`/api/aw/jobs/${jobName}/trigger`,
"frontend.jobs.aw.trigger",
);
export const triggerWWIJob = (jobName: string) =>
post<{ triggered: boolean; job_name: string }>(
`/api/jobs/wwi/${jobName}/trigger`,
post<{ status: string; job: string }>(
`/api/wwi/jobs/${jobName}/trigger`,
"frontend.jobs.wwi.trigger",
);

View File

@@ -126,11 +126,12 @@ export type WWIScenario = {
export type AWAnomalyPoint = {
date: string;
revenue: number;
rolling_mean: number;
lower_band: number;
upper_band: number;
// null during the rolling-window warm-up period
rolling_mean: number | null;
lower_band: number | null;
upper_band: number | null;
is_anomaly: boolean;
z_score: number;
z_score: number | null;
direction: "high" | "low" | null;
};

View File

@@ -72,8 +72,16 @@ export default function AnomalyDetection() {
anomalyRevenue: p.is_anomaly ? p.revenue : null,
}));
const domainMin = Math.min(...series.map((p) => p.lower_band)) * 0.95;
const domainMax = Math.max(...series.map((p) => p.upper_band)) * 1.05;
// Bands are null during the rolling-window warm-up; ignore those and fall
// back to revenue so anomaly points are never clipped off the chart.
const lows = series
.map((p) => (p.lower_band ?? p.revenue))
.filter((v): v is number => v != null);
const highs = series
.map((p) => (p.upper_band ?? p.revenue))
.filter((v): v is number => v != null);
const domainMin = lows.length ? Math.min(...lows) * 0.95 : 0;
const domainMax = highs.length ? Math.max(...highs) * 1.05 : 0;
return (
<div className="flex flex-col gap-6 max-w-[1100px] mx-auto">
@@ -228,16 +236,16 @@ export default function AnomalyDetection() {
<td className="font-mono text-sm">{p.date}</td>
<td>{money.format(p.revenue)}</td>
<td className="text-[rgba(233,244,255,0.5)]">
{money.format(p.rolling_mean)}
{p.rolling_mean != null ? money.format(p.rolling_mean) : "—"}
</td>
<td
className={
Math.abs(p.z_score) > 3
(p.z_score != null && Math.abs(p.z_score) > 3)
? "text-red-400 font-semibold"
: "text-amber-400"
}
>
{p.z_score.toFixed(2)}
{p.z_score != null ? p.z_score.toFixed(2) : "—"}
</td>
<td>{directionBadge(p.direction)}</td>
</tr>

View File

@@ -13,7 +13,6 @@ const AW_JOBS = [
const WWI_JOBS = [
{ id: "reorder", label: "Reorder Recommendations" },
{ id: "supplier_scores", label: "Supplier Scores" },
{ id: "events", label: "Business Events" },
{ id: "data_quality", label: "Data Quality" },
];