diff --git a/web-app/app/home/chart.tsx b/web-app/app/home/chart.tsx index 4294d31..c0bd2c4 100644 --- a/web-app/app/home/chart.tsx +++ b/web-app/app/home/chart.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { type DailyOrderCount, type RobotDayRow, + type RobotTotal, formatDay, } from "./data"; @@ -290,38 +291,31 @@ function HoverTooltip({ // --- Legend --- interface ChartLegendProps { - robotNames: string[]; - colorByRobot: Map; - totalByRobot: Map; - errorsByRobot: Map; + title: string; + totals: RobotTotal[]; } -function ChartLegend({ - robotNames, - colorByRobot, - totalByRobot, - errorsByRobot, -}: ChartLegendProps) { - const sorted = [...robotNames].sort( - (a, b) => (totalByRobot.get(b) ?? 0) - (totalByRobot.get(a) ?? 0) +function ChartLegend({ title, totals }: ChartLegendProps) { + const sorted = [...totals].sort( + (a, b) => b.count - b.errorCount - (a.count - a.errorCount) ); return (
-
Last 7 days
+
{title}
Successes (Errors)
    - {sorted.map((name) => { - const errors = errorsByRobot.get(name) ?? 0; + {sorted.map((t) => { + const okCount = Math.max(0, t.count - t.errorCount); return ( -
  • +
  • - {name}: {totalByRobot.get(name) ?? 0} - {errors > 0 && ( - ({errors}) + {t.robotName}: {okCount} + {t.errorCount > 0 && ( + ({t.errorCount}) )}
  • @@ -336,34 +330,49 @@ function ChartLegend({ export function OrdersChart({ data, + totals14d, onBarClick, selected, }: { data: DailyOrderCount[]; + totals14d: RobotTotal[]; onBarClick: (day: Date, row: RobotDayRow) => void; selected: { dayMs: number; robotId: string } | null; }) { const [hover, setHover] = useState(null); - const days = [...data].slice(0, 7).reverse(); + const dayMap = new Map(data.map((d) => [d.day.getTime(), d])); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const firstDay = new Date(today); + firstDay.setDate(firstDay.getDate() - 7); + const days: DailyOrderCount[] = []; + for (let dt = new Date(firstDay); dt.getTime() <= today.getTime(); ) { + const dayDate = new Date(dt); + days.push(dayMap.get(dayDate.getTime()) ?? { day: dayDate, rows: [] }); + dt.setDate(dt.getDate() + 1); + } const robotNames = [ ...new Set(days.flatMap((d) => d.rows.map((r) => r.robotName))), ].sort(); const colorByRobot = new Map( robotNames.map((name) => [name, colorFor(name)]) ); - const totalByRobot = new Map(); - const errorsByRobot = new Map(); + const totals7dByRobot = new Map(); for (const d of days) { for (const r of d.rows) { - const okCount = Math.max(0, r.count - r.errorCount); - totalByRobot.set(r.robotName, (totalByRobot.get(r.robotName) ?? 0) + okCount); - errorsByRobot.set( - r.robotName, - (errorsByRobot.get(r.robotName) ?? 0) + r.errorCount - ); + const cur = totals7dByRobot.get(r.robotId) ?? { + robotId: r.robotId, + robotName: r.robotName, + count: 0, + errorCount: 0, + }; + cur.count += r.count; + cur.errorCount += r.errorCount; + totals7dByRobot.set(r.robotId, cur); } } + const totals7d = [...totals7dByRobot.values()]; const width = 800; const height = 230; @@ -511,12 +520,10 @@ export function OrdersChart({ )} - +
    + + +
); } diff --git a/web-app/app/home/data.ts b/web-app/app/home/data.ts index ea30a3e..88ca360 100644 --- a/web-app/app/home/data.ts +++ b/web-app/app/home/data.ts @@ -21,6 +21,13 @@ export interface RobotDayRow { errorCount: number; } +export interface RobotTotal { + robotId: string; + robotName: string; + count: number; + errorCount: number; +} + export interface DailyOrderCount { day: Date; rows: RobotDayRow[]; @@ -161,7 +168,7 @@ export async function loadDailyOrderCounts( ): Promise { const nameById = new Map(machines.map((m) => [m.id, m.name])); const tz = browserTimezone(); - const since = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000); + const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const results = await runMQL<{ time: Date | string; @@ -236,6 +243,64 @@ export async function loadDailyOrderCounts( .sort((a, b) => b.day.getTime() - a.day.getTime()); } +export async function loadRobotTotalsLastNDays( + client: VIAM.ViamClient, + machines: Machine[], + days: number +): Promise { + const nameById = new Map(machines.map((m) => [m.id, m.name])); + const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000); + + const results = await runMQL<{ + robot_id: string; + order_ok: boolean | null; + value: number; + }>(client, [ + { + $match: { + location_id: LOCATION_ID, + component_name: RESOURCE_NAME, + time_received: { $gte: since }, + }, + }, + { + $group: { + _id: { + robot_id: "$robot_id", + order_ok: "$data.readings.order_ok", + }, + value: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + robot_id: "$_id.robot_id", + order_ok: "$_id.order_ok", + value: 1, + }, + }, + ]); + + type Tally = { count: number; errorCount: number }; + const byRobot = new Map(); + for (const row of results) { + if (!nameById.has(row.robot_id)) continue; + const tally = byRobot.get(row.robot_id) ?? { count: 0, errorCount: 0 }; + tally.count += row.value; + if (row.order_ok === false) tally.errorCount += row.value; + byRobot.set(row.robot_id, tally); + } + return [...byRobot.entries()] + .map(([robotId, tally]) => ({ + robotId, + robotName: nameById.get(robotId) ?? robotId, + count: tally.count, + errorCount: tally.errorCount, + })) + .sort((a, b) => a.robotName.localeCompare(b.robotName)); +} + export async function loadLeaderboard( client: VIAM.ViamClient, groupByField: string, diff --git a/web-app/app/page.tsx b/web-app/app/page.tsx index b44e89f..8321ba4 100644 --- a/web-app/app/page.tsx +++ b/web-app/app/page.tsx @@ -16,12 +16,14 @@ import { type OrderRecord, type LeaderboardEntry, type Panel, + type RobotTotal, panelKey, listMachines, loadDailyOrderCounts, loadLeaderboard, loadOrdersForDay, loadErrorsLast7Days, + loadRobotTotalsLastNDays, } from "./home/data"; import { OrdersChart } from "./home/chart"; import { OrdersPanel } from "./home/orders-panel"; @@ -38,6 +40,7 @@ export default function Home() { const [error, setError] = useState(null); const [machines, setMachines] = useState([]); const [orderCounts, setOrderCounts] = useState(null); + const [totals14d, setTotals14d] = useState([]); const [customerLeaderboard, setCustomerLeaderboard] = useState< LeaderboardEntry[] | null >(null); @@ -107,6 +110,11 @@ export default function Home() { .catch((e) => console.error("failed to load daily order counts:", e) ); + loadRobotTotalsLastNDays(currentClient, currentMachines, 14) + .then((d) => !cancelled && setTotals14d(d)) + .catch((e) => + console.error("failed to load 14-day robot totals:", e) + ); loadLeaderboard(currentClient, "data.readings.customer_name") .then((d) => !cancelled && setCustomerLeaderboard(d)) .catch((e) => @@ -272,6 +280,7 @@ export default function Home() { <>