diff --git a/next.config.js b/next.config.js index f2bc28bcd2bb..0bcc205b3112 100644 --- a/next.config.js +++ b/next.config.js @@ -21,7 +21,6 @@ const config = { webpackMemoryOptimizations: true, preloadEntriesOnStart: false, turbopackFileSystemCacheForDev: false, - turbopackMemoryLimit: 4096, }, images: { unoptimized: true, diff --git a/package.json b/package.json index 98a676fef060..0083356427c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.10.2", + "version": "10.10.3", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { @@ -8,7 +8,7 @@ }, "license": "AGPL-3.0", "engines": { - "node": "^22.22.2" + "node": "^22.22.0" }, "repository": { "type": "git", @@ -134,12 +134,12 @@ "@testing-library/user-event": "14.6.1", "@types/react": "19.2.14", "@types/react-dom": "19.2.5", - "@vitest/browser-playwright": "4.1.10", - "@vitest/coverage-v8": "4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/coverage-v8": "4.1.11", "eslint": "^9.39.4", "eslint-config-next": "^16.3.4", "eslint-config-prettier": "^10.1.8", - "jsdom": "30.0.1", + "jsdom": "29.0.1", "msw": "2.15.0", "msw-storybook-addon": "3.0.0", "playwright": "1.63.0", @@ -147,7 +147,7 @@ "storybook": "10.5.10", "typescript": "5.9.3", "vite": "8.2.2", - "vitest": "4.1.10" + "vitest": "4.1.11" }, "msw": { "workerDirectory": [ @@ -160,4 +160,4 @@ "sharp": "^0.35.0", "monaco-editor/dompurify": "^3.4.14" } -} \ No newline at end of file +} diff --git a/public/version.json b/public/version.json index c078a41b9ff0..feed56cacfd3 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.10.2" + "version": "10.10.3" } \ No newline at end of file diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index dab26621d981..412b52927156 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -140,4 +140,7 @@ export const CippTranslations = { RequestsPriorHour: 'Requests In Prior Hour', BaselinePerHour: 'Baseline / Hr', SharePct: 'Share %', + ExecutedRequests: 'Executed', + ServedRequests: 'Served (incl. cached)', + EgressToday: 'Egress Today', } diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 09316ed884be..b8d2e9d25dcc 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -901,6 +901,7 @@ export const useCippUserActions = () => { ], confirmText: 'Select a SharePoint site and where to create the OneDrive shortcut:', multiPost: false, + allowResubmit: true, condition: () => canWriteUser, }, { diff --git a/src/components/CippIntegrations/CippApiClientManagement.jsx b/src/components/CippIntegrations/CippApiClientManagement.jsx index 167011880780..3e06fe1f990a 100644 --- a/src/components/CippIntegrations/CippApiClientManagement.jsx +++ b/src/components/CippIntegrations/CippApiClientManagement.jsx @@ -82,6 +82,35 @@ const CippApiClientManagement = () => { queryKey: "CustomRoleList", }); + // Authoritative per-client egress (today) from Craft's accounting table. Self-hides (Enabled:false) + // when accounting is off / not hosted, in which case the column shows "-". + const egressUsage = ApiGetCall({ + url: "/api/ListApiEgress", + queryKey: "ApiEgressUsage", + }); + + // Merge the client list with egress so the table can show a per-client "Egress (today)" column. + // The list is small, so this drives the table from `data` (client-side) rather than the server api. + const clientRows = useMemo(() => { + const clients = apiClients.data?.pages?.[0]?.Results || []; + const usage = egressUsage.data?.Results?.Enabled ? egressUsage.data.Results.Clients || [] : []; + const byAppId = new Map(usage.map((c) => [String(c.AppId).toLowerCase(), c])); + const fmtBytes = (b) => + b == null + ? "-" + : b >= 1073741824 + ? `${(b / 1073741824).toFixed(1)} GB` + : b >= 1048576 + ? `${(b / 1048576).toFixed(1)} MB` + : b >= 1024 + ? `${(b / 1024).toFixed(1)} KB` + : `${b} B`; + return clients.map((c) => { + const e = byAppId.get(String(c.ClientId).toLowerCase()); + return { ...c, EgressToday: e ? fmtBytes(e.Bytes) : "-", EgressSheddedToday: e ? e.Shed : 0 }; + }); + }, [apiClients.data, egressUsage.data]); + // MCP-enabled clients whose role restricts sign-in to specific IPs. Those restrictions apply to // MCP traffic (which runs as the signed-in user), so an AI client's cloud egress IPs get blocked. const mcpRoleIpWarnings = useMemo(() => { @@ -462,12 +491,21 @@ const CippApiClientManagement = () => { { + apiClients.refetch?.(); + egressUsage.refetch?.(); }} - simpleColumns={["Enabled", "MCPAllowed", "AppName", "ClientId", "Role", "IPRange"]} + simpleColumns={[ + "Enabled", + "MCPAllowed", + "AppName", + "ClientId", + "Role", + "IPRange", + "EgressToday", + ]} queryKey={`ApiClients`} /> diff --git a/src/components/CippIntegrations/CippApiEgressCard.jsx b/src/components/CippIntegrations/CippApiEgressCard.jsx new file mode 100644 index 000000000000..251ee318aad1 --- /dev/null +++ b/src/components/CippIntegrations/CippApiEgressCard.jsx @@ -0,0 +1,209 @@ +import { useMemo, useState } from "react"; +import { + Card, + CardContent, + CardHeader, + Stack, + ToggleButton, + ToggleButtonGroup, + Typography, + useTheme, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { + Area, + AreaChart, + CartesianGrid, + Legend, + PolarAngleAxis, + RadialBar, + RadialBarChart, + ResponsiveContainer, + Tooltip as RechartsTooltip, + XAxis, + YAxis, +} from "recharts"; +import { ApiGetCall } from "../../api/ApiCall"; + +// Authoritative per-client API egress from Craft's CraftEgressAccounting table (via /api/ListApiEgress): +// a used-of-cap gauge for the instance total, and a stacked-by-client trend over the selected window. +// Self-hides when accounting is off / not hosted. Reused on the Diagnostics and Integrations pages. + +const RANGE_OPTIONS = [ + { label: "24h", hours: 24 }, + { label: "3d", hours: 72 }, + { label: "7d", hours: 168 }, +]; + +const formatBytes = (b) => { + if (b == null) return "-"; + if (b >= 1073741824) return `${(b / 1073741824).toFixed(2)} GB`; + if (b >= 1048576) return `${(b / 1048576).toFixed(1)} MB`; + if (b >= 1024) return `${(b / 1024).toFixed(1)} KB`; + return `${b} B`; +}; + +const formatBucketTime = (iso, hours) => { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return ""; + const hh = String(d.getUTCHours()).padStart(2, "0"); + const mm = String(d.getUTCMinutes()).padStart(2, "0"); + if (hours > 48) return `${d.getUTCMonth() + 1}/${d.getUTCDate()} ${hh}:${mm}`; + return `${hh}:${mm}`; +}; + +export const CippApiEgressCard = () => { + const theme = useTheme(); + const [hours, setHours] = useState(24); + + const query = ApiGetCall({ + url: "/api/ListApiEgress", + data: { Hours: String(hours) }, + queryKey: `ApiEgressUsage-${hours}`, + }); + const r = query.data?.Results; + + const palette = useMemo( + () => [ + theme.palette.primary.main, + theme.palette.info.main, + theme.palette.success.main, + theme.palette.warning.main, + theme.palette.secondary.main, + theme.palette.error.main, + ], + [theme] + ); + + // Stacked chart rows: bytes -> MB per client per bucket. + const chartData = useMemo(() => { + const ids = r?.ClientIds ?? []; + return (r?.Trend ?? []).map((bucket) => { + const row = { time: formatBucketTime(bucket.BucketStartUtc, hours) }; + ids.forEach((id) => { + row[id] = Math.round(((bucket[id] ?? 0) / 1048576) * 100) / 100; + }); + return row; + }); + }, [r, hours]); + + // Query resolved but accounting is off / no data: render nothing. + if (query.isSuccess && !r?.Enabled) return null; + + const clientIds = r?.ClientIds ?? []; + const capBytes = r?.CapBytes ?? 0; + const pct = r?.PctOfCap ?? (capBytes > 0 ? Math.round(((r?.BytesToday ?? 0) / capBytes) * 100) : 0); + const capReached = r?.CapReachedUtc != null; + const gaugeColor = capReached + ? theme.palette.error.main + : pct >= 80 + ? theme.palette.warning.main + : theme.palette.success.main; + const gaugeData = [{ name: "used", value: Math.min(100, Math.max(0, pct)), fill: gaugeColor }]; + + return ( + + v && setHours(v)} + > + {RANGE_OPTIONS.map((o) => ( + + {o.label} + + ))} + + } + /> + + + {/* ── Used-of-cap gauge / total ── */} + + + {capBytes > 0 ? ( + <> + + + + + + + + {pct}% + + + {formatBytes(r?.BytesToday)} of {formatBytes(capBytes)} today + + + ) : ( + + {formatBytes(r?.BytesToday)} + + used today (accounting only, no cap) + + + )} + {capReached && ( + + Cap reached - requests shed with 429 ({r?.ShedRequests} today) + + )} + {!capReached && r?.ShedRequests > 0 && ( + + {r.ShedRequests} shed today + + )} + + + + {/* ── Stacked per-client trend ── */} + + {chartData.length === 0 ? ( + + No egress recorded in this window yet. + + ) : ( + + + + + + [`${value} MB`, r?.ClientNames?.[name] ?? name]} + /> + r?.ClientNames?.[name] ?? name} /> + {clientIds.map((id, i) => ( + + ))} + + + )} + + + + + ); +}; + +export default CippApiEgressCard; diff --git a/src/pages/cipp/advanced/container-management/diagnostics.jsx b/src/pages/cipp/advanced/container-management/diagnostics.jsx index 9ddc66d2d9a3..2692bda3f911 100644 --- a/src/pages/cipp/advanced/container-management/diagnostics.jsx +++ b/src/pages/cipp/advanced/container-management/diagnostics.jsx @@ -45,6 +45,7 @@ import { sortDiagnosticsChecks, buildClientLogQuery, buildRequestSeries, + buildStackedSeries, getCheckLabel, formatBytes, } from "../../../../utils/instance-diagnostics"; @@ -62,9 +63,13 @@ const WINDOWS = [ // one-off chip renderer for this page. const STATUS_LABEL = { FAIL: "Failed", WARN: "Warning", PASS: "Passed", INFO: "Info" }; -const formatChartTime = (bucket, hours) => { - // Bucket is a naive 'yyyy-MM-ddTHH:mm' UTC string — force UTC parsing. - const d = new Date(`${bucket}:00Z`); +// Health buckets are naive 'yyyy-MM-ddTHH:mm' UTC strings, egress buckets full ISO — both +// become epoch ms so the 5 and 15 minute strips share one numeric time axis. +const bucketEpoch = (bucket) => + bucket ? new Date(bucket.endsWith("Z") ? bucket : `${bucket}:00Z`).getTime() : null; + +const formatChartTime = (epoch, hours) => { + const d = new Date(epoch); if (hours <= 24) { return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); } @@ -72,8 +77,8 @@ const formatChartTime = (bucket, hours) => { }; // Always the full date + time — a bare clock time is ambiguous on a multi-day window. -const formatFullDateTime = (bucket) => { - const d = new Date(`${bucket}:00Z`); +const formatFullDateTime = (epoch) => { + const d = new Date(epoch); return d.toLocaleString("en-US", { month: "short", day: "numeric", @@ -146,7 +151,7 @@ const buildEventRows = (events) => const busiest = topClients[0]; return { id: `${e.Bucket}-${i}`, - Time: formatFullDateTime(e.Bucket), + Time: formatFullDateTime(bucketEpoch(e.Bucket)), Event: e.Type === "boot" ? "Container restarted" : "Out of memory", Outage: e.Type === "boot" && e.GapMinutes != null ? `${e.GapMinutes} min` : "", BusiestClient: busiest ? busiest.AppName || busiest.AppId : "—", @@ -201,21 +206,42 @@ const EventsCard = ({ events, hours, isFetching, refreshFunction }) => { ); }; -const ApiClientsCard = ({ buckets, hours, isFetching, refreshFunction }) => { - const rows = useMemo( - () => - aggregateDiagnosticsClients(buckets).map((c) => ({ +const ApiClientsCard = ({ buckets, hours, egressClients, isFetching, refreshFunction }) => { + // Executed comes from the log (requests that ran PowerShell), Served from Craft's wire + // accounting, so a cache-only client shows up with Served but no Executed. + const rows = useMemo(() => { + const egressById = new Map((egressClients ?? []).map((c) => [c.AppId, c])); + const logRows = aggregateDiagnosticsClients(buckets).map((c) => { + const egress = egressById.get(c.AppId); + egressById.delete(c.AppId); + return { ...c, Client: c.AppName || c.AppId, - })), - [buckets] - ); + ExecutedRequests: c.Count, + ...(egress ? { ServedRequests: egress.Requests, EgressToday: formatBytes(egress.Bytes) } : {}), + }; + }); + const egressOnly = Array.from(egressById.values()).map((c) => ({ + AppId: c.AppId, + AppName: c.AppName, + Client: c.AppName || c.AppId, + ExecutedRequests: 0, + ServedRequests: c.Requests, + EgressToday: formatBytes(c.Bytes), + SharePct: 0, + })); + return logRows.concat(egressOnly); + }, [buckets, egressClients]); + + const columns = egressClients + ? ["Client", "IP", "ExecutedRequests", "ServedRequests", "EgressToday", "SharePct"] + : ["Client", "IP", "ExecutedRequests", "SharePct"]; return ( ({ border: `1px solid ${t.palette.divider}`, borderRadius: 4, }, + labelFormatter: (value) => formatFullDateTime(value), }); -const RequestsChart = ({ data, series, theme: t }) => ( +const clientColor = (t, index) => t.palette[CLIENT_COLOR_KEYS[index % CLIENT_COLOR_KEYS.length]].main; + +const RequestsChart = ({ data, series, xAxis, theme: t }) => ( - + @@ -272,42 +301,47 @@ const RequestsChart = ({ data, series, theme: t }) => ( dataKey={s.AppId} name={s.AppName} stackId="clients" - fill={t.palette[CLIENT_COLOR_KEYS[i % CLIENT_COLOR_KEYS.length]].main} + barSize={6} + fill={clientColor(t, i)} /> ))} {data.some((row) => row.Other > 0) && ( - + )} ); -const EgressChart = ({ data, todayBytes, capBytes, theme: t }) => { - const caption = - todayBytes == null - ? null - : capBytes - ? `Today: ${formatBytes(todayBytes)} of ${formatBytes(capBytes)} (${Math.round( - (todayBytes / capBytes) * 100 - )}%)` - : `Today: ${formatBytes(todayBytes)}`; - - return ( - - - - - - - [`${value} MB`, "Egress"]} /> - - - - - ); -}; +const EgressChart = ({ data, series, caption, colorIndex, xAxis, theme: t }) => ( + + + + + + + [`${Number(value).toFixed(1)} MB`, name]} + /> + + {series.map((s, i) => ( + + ))} + {data.some((row) => row.Other > 0) && ( + + )} + + +); -const HeapChart = ({ data, heapCapMb, eventMarkers, theme: t }) => { +const HeapChart = ({ data, heapCapMb, eventMarkers, xAxis, theme: t }) => { const peak = data.reduce((max, d) => (d.Heap != null && d.Heap > max ? d.Heap : max), 0); const domainMax = Math.max(heapCapMb || 0, peak) * 1.05; @@ -315,7 +349,7 @@ const HeapChart = ({ data, heapCapMb, eventMarkers, theme: t }) => { - + @@ -340,7 +374,7 @@ const HeapChart = ({ data, heapCapMb, eventMarkers, theme: t }) => { {eventMarkers.map((e, i) => ( { ); }; -const PoolChart = ({ data, theme: t }) => ( +const PoolChart = ({ data, xAxis, theme: t }) => ( - + - + { const buckets = useMemo(() => timelineQuery.data?.Results?.Buckets ?? [], [timelineQuery.data]); const events = useMemo(() => timelineQuery.data?.Results?.Events ?? [], [timelineQuery.data]); const heapCapMb = timelineQuery.data?.Results?.HeapCapMb ?? null; - const egressAvailable = timelineQuery.data?.Results?.EgressAvailable ?? false; - const egressCapBytes = timelineQuery.data?.Results?.EgressCapBytes ?? null; + const egress = timelineQuery.data?.Results?.Egress; + const egressAvailable = egress?.Available ?? false; const isFetching = checksQuery.isFetching || timelineQuery.isFetching; const chartData = useMemo( () => buckets.map((b) => ({ ...b, - time: formatChartTime(b.Bucket, hours), + t: bucketEpoch(b.Bucket), Heap: b.HeapMb ?? b.HeapMbLive ?? null, })), - [buckets, hours] + [buckets] ); const requestSeries = useMemo(() => buildRequestSeries(buckets), [buckets]); const requestChartData = useMemo( - () => requestSeries.data.map((row) => ({ ...row, time: formatChartTime(row.Bucket, hours) })), - [requestSeries, hours] + () => requestSeries.data.map((row) => ({ ...row, t: bucketEpoch(row.Bucket) })), + [requestSeries] + ); + // Colour is keyed on the request chart's client order, so a client looks the same in both strips. + const clientColorIndex = useMemo( + () => new Map(requestSeries.series.map((s, i) => [s.AppId, i])), + [requestSeries] ); + const egressSeries = useMemo( + () => + buildStackedSeries(egress?.Buckets ?? [], { + bucketKey: "BucketStart", + valueKey: "Bytes", + order: requestSeries.series.map((s) => s.AppId), + }), + [egress, requestSeries] + ); const egressChartData = useMemo( () => - buckets.map((b) => ({ - time: formatChartTime(b.Bucket, hours), - EgressMb: b.EgressBytes != null ? Math.round((b.EgressBytes / 1048576) * 10) / 10 : null, - })), - [buckets, hours] + egressSeries.data.map(({ Bucket, ...values }) => { + const row = { t: bucketEpoch(Bucket) }; + for (const [key, value] of Object.entries(values)) { + row[key] = Math.round(((Number(value) || 0) / 1048576) * 10) / 10; + } + return row; + }), + [egressSeries] ); - // Newest bucket that actually carries a reading - later buckets in the window can be empty. - const egressToday = [...buckets].reverse().find((b) => b.EgressBytesToday != null)?.EgressBytesToday ?? null; + const egressCaption = egress + ? [ + egress.Enforcing + ? `Today: ${formatBytes(egress.TodayBytes)} of ${formatBytes(egress.CapBytes)} (${ + egress.CapBytes > 0 ? Math.round((egress.TodayBytes / egress.CapBytes) * 100) : 0 + }%)` + : `Today: ${formatBytes(egress.TodayBytes)}`, + egress.TodayShed > 0 ? ` \u00b7 ${egress.TodayShed} refused (429)` : "", + ].join("") + : null; + + // 5 and 15 minute buckets can never share a category axis, so every strip is drawn on one + // numeric time axis with the same domain and ticks. + const timeDomain = useMemo(() => { + const stamps = chartData + .map((d) => d.t) + .concat(egressChartData.map((d) => d.t)) + .filter((t) => t != null); + if (stamps.length === 0) return null; + // Pad by a bucket width either side so the first and last bars are not clipped. + return [Math.min(...stamps) - 5 * 60000, Math.max(...stamps) + 15 * 60000]; + }, [chartData, egressChartData]); + + const timeTicks = useMemo(() => { + if (!timeDomain) return []; + const [from, to] = timeDomain; + const step = (to - from) / 7; + return Array.from({ length: 8 }, (_, i) => Math.round(from + i * step)); + }, [timeDomain]); + + const timeAxis = { + type: "number", + dataKey: "t", + scale: "time", + domain: timeDomain ?? ["dataMin", "dataMax"], + ticks: timeTicks, + allowDataOverflow: true, + tickFormatter: (value) => formatChartTime(value, hours), + tick: { fontSize: 11 }, + tickMargin: 8, + }; const showPoolChart = buckets.some( (b) => (b.PoolExhaustedCount ?? 0) > 0 || (b.MaxLimiterWaitMs ?? 0) >= 10000 @@ -435,14 +525,14 @@ const Page = () => { [chartData] ); - // Only draw a marker for events whose bucket landed in this window's samples — - // an event just outside the sample set has no x-axis category to attach to. - const eventMarkers = useMemo(() => { - const timeByBucket = new Map(buckets.map((b) => [b.Bucket, formatChartTime(b.Bucket, hours)])); - return events - .map((e) => ({ ...e, time: timeByBucket.get(e.Bucket) })) - .filter((e) => e.time != null); - }, [events, buckets, hours]); + // Markers outside the drawn window would sit on the axis edge, so drop them. + const eventMarkers = useMemo( + () => + events + .map((e) => ({ ...e, t: bucketEpoch(e.Bucket) })) + .filter((e) => e.t != null && timeDomain && e.t >= timeDomain[0] && e.t <= timeDomain[1]), + [events, timeDomain] + ); const handleRefresh = () => { queryClient.invalidateQueries({ queryKey: [`InstanceDiagnosticsChecks-${hours}`] }); @@ -515,12 +605,19 @@ const Page = () => { ) : ( - + {egressAvailable && ( )} @@ -528,9 +625,12 @@ const Page = () => { data={chartData} heapCapMb={heapCapMb} eventMarkers={eventMarkers} + xAxis={timeAxis} theme={theme} /> - {showPoolChart && } + {showPoolChart && ( + + )} )} @@ -548,11 +648,13 @@ const Page = () => { )} + diff --git a/src/pages/cipp/integrations/configure.jsx b/src/pages/cipp/integrations/configure.jsx index 0e3f57a49c63..5345132a94f2 100644 --- a/src/pages/cipp/integrations/configure.jsx +++ b/src/pages/cipp/integrations/configure.jsx @@ -27,6 +27,7 @@ import CippIntegrationTenantMapping from "../../../components/CippIntegrations/C import CippIntegrationFieldMapping from "../../../components/CippIntegrations/CippIntegrationFieldMapping"; import { CippCardTabPanel } from "../../../components/CippComponents/CippCardTabPanel"; import CippApiClientManagement from "../../../components/CippIntegrations/CippApiClientManagement"; +import { CippApiEgressCard } from "../../../components/CippIntegrations/CippApiEgressCard"; import CippApiDocumentation from "../../../components/CippIntegrations/CippApiDocumentation"; function tabProps(index) { @@ -304,7 +305,10 @@ const Page = () => { {extension?.id === "cippapi" ? ( - + + + + ) : ( )} diff --git a/src/pages/identity/administration/users/user/onedrive-shortcuts.jsx b/src/pages/identity/administration/users/user/onedrive-shortcuts.jsx index ed23ef80dab8..6d1b0b2df48b 100644 --- a/src/pages/identity/administration/users/user/onedrive-shortcuts.jsx +++ b/src/pages/identity/administration/users/user/onedrive-shortcuts.jsx @@ -147,6 +147,7 @@ const Page = () => { destination: { label: 'OneDrive root', value: 'root' }, }} relatedQueryKeys={[shortcutsQueryKey]} + allowResubmit fields={[ { type: 'autoComplete', diff --git a/src/pages/teams-share/teams/business-voice/index.jsx b/src/pages/teams-share/teams/business-voice/index.jsx index b5a6a69acbcf..54f4a90e2faa 100644 --- a/src/pages/teams-share/teams/business-voice/index.jsx +++ b/src/pages/teams-share/teams/business-voice/index.jsx @@ -18,70 +18,73 @@ const Page = () => { const actions = [ // the modal dropdowns that were added below may not exist yet, and will need to be tested. { - label: "Assign User", - type: "POST", + label: 'Assign User', + type: 'POST', icon: , - url: "/api/ExecTeamsVoicePhoneNumberAssignment", + url: '/api/ExecTeamsVoicePhoneNumberAssignment', data: { - PhoneNumber: "TelephoneNumber", - PhoneNumberType: "NumberType", + PhoneNumber: 'TelephoneNumber', + PhoneNumberType: 'NumberType', locationOnly: false, }, fields: [ { - type: "autoComplete", - name: "input", - label: "Select User", + type: 'autoComplete', + name: 'input', + label: 'Select User', multiple: false, creatable: false, api: { - url: "/api/ListGraphRequest", - queryKey: "TeamsVoiceAssignableUsers", - dataKey: "Results", + url: '/api/ListGraphRequest', + queryKey: 'TeamsVoiceAssignableUsers', + dataKey: 'Results', data: { - Endpoint: "users", + Endpoint: 'users', manualPagination: true, - $select: "id,userPrincipalName,displayName", + $select: 'id,userPrincipalName,displayName', $count: true, - $orderby: "displayName", + $orderby: 'displayName', $top: 999, }, - labelField: (input) => `${input.displayName} (${input.userPrincipalName})`, - valueField: "userPrincipalName", + labelField: (input) => + `${input.displayName} (${input.userPrincipalName})`, + valueField: 'userPrincipalName', }, }, ], - confirmText: "Select the User to assign the phone number to.", + confirmText: 'Select the User to assign the phone number to.', + multiPost: false, }, { - label: "Unassign User", - type: "POST", + label: 'Unassign User', + type: 'POST', icon: , - url: "/api/ExecRemoveTeamsVoicePhoneNumberAssignment", + url: '/api/ExecRemoveTeamsVoicePhoneNumberAssignment', data: { - PhoneNumber: "TelephoneNumber", - AssignedTo: "AssignedTo", - PhoneNumberType: "NumberType", + PhoneNumber: 'TelephoneNumber', + AssignedTo: 'AssignedTo', + PhoneNumberType: 'NumberType', }, - confirmText: "Are you sure you want to remove the assignment?", + confirmText: 'Are you sure you want to remove the assignment?', + multiPost: false, }, { - label: "Set Emergency Location", - type: "POST", + label: 'Set Emergency Location', + type: 'POST', icon: , - url: "/api/ExecTeamsVoicePhoneNumberAssignment", + url: '/api/ExecTeamsVoicePhoneNumberAssignment', data: { - PhoneNumber: "TelephoneNumber", + PhoneNumber: 'TelephoneNumber', locationOnly: true, }, fields: [ { - type: "autoComplete", - name: "input", - label: "Emergency Location", + type: 'autoComplete', + name: 'input', + label: 'Emergency Location', api: { - url: "/api/ListTeamsLisLocation", - queryKey: "TeamsLisLocations", + url: '/api/ListTeamsLisLocation', + queryKey: 'TeamsLisLocations', // Description is optional on a location, so fall back to the place name and // then the street address rather than rendering "No label found". labelField: (location) => @@ -89,15 +92,16 @@ const Page = () => { location.Location || [location.HouseNumber, location.StreetName, location.City] .filter(Boolean) - .join(" ") || + .join(' ') || location.LocationId, - valueField: "LocationId", + valueField: 'LocationId', }, }, ], - confirmText: "Select the Emergency Location.", + confirmText: 'Select the Emergency Location.', + multiPost: false, }, - ]; + ] const offCanvas = { extendedInfoFields: [ diff --git a/src/utils/instance-diagnostics.js b/src/utils/instance-diagnostics.js index 8533507c9df9..8ec261baf0f2 100644 --- a/src/utils/instance-diagnostics.js +++ b/src/utils/instance-diagnostics.js @@ -9,6 +9,7 @@ export const CHECK_LABELS = { "pool-exhausted": "HTTP worker pool", "stalled-runs": "Stalled runs", "api-clients": "API clients", + egress: "API egress", restarts: "Container restarts", orchestrator: "Orchestrator", "container-log": "Platform container log", @@ -48,35 +49,51 @@ export const sortDiagnosticsChecks = (checks) => (a, b) => (STATUS_ORDER[a.Status] ?? 99) - (STATUS_ORDER[b.Status] ?? 99) ); -// Stacked-bar series for the Health Timeline's "API Requests" chart: the top N clients by -// total Count over the window get their own series, everything else is folded into "Other". -export const buildRequestSeries = (buckets, topN = 4) => { +// Stacked-bar series for a Health Timeline strip: the top N clients by total value over the +// window get their own series, everything else is folded into "Other". +// `order` pins leading series to a caller-supplied AppId order so a client keeps the same +// colour across strips; `bucketKey`/`valueKey` pick the bucket timestamp and the value to sum. +export const buildStackedSeries = ( + buckets, + { bucketKey = "Bucket", valueKey = "Count", topN = 4, order } = {} +) => { const totals = new Map(); for (const bucket of buckets ?? []) { for (const client of bucket?.Clients ?? []) { if (!client?.AppId) continue; - const existing = totals.get(client.AppId) ?? { AppId: client.AppId, AppName: client.AppName, Count: 0 }; - existing.Count += Number(client.Count) || 0; + const existing = totals.get(client.AppId) ?? { + AppId: client.AppId, + AppName: client.AppName, + Total: 0, + }; + existing.Total += Number(client[valueKey]) || 0; if (client.AppName) existing.AppName = client.AppName; totals.set(client.AppId, existing); } } - const topClients = Array.from(totals.values()) - .sort((a, b) => b.Count - a.Count) - .slice(0, topN); + + const ordered = []; + for (const appId of order ?? []) { + const client = totals.get(appId); + if (client) ordered.push(client); + } + const rest = Array.from(totals.values()) + .filter((c) => !ordered.includes(c)) + .sort((a, b) => b.Total - a.Total); + const topClients = ordered.concat(rest).slice(0, topN); const topIds = new Set(topClients.map((c) => c.AppId)); const data = (buckets ?? []).map((bucket) => { - const row = { Bucket: bucket.Bucket }; + const row = { Bucket: bucket[bucketKey] }; for (const id of topIds) row[id] = 0; let other = 0; for (const client of bucket?.Clients ?? []) { if (!client?.AppId) continue; - const count = Number(client.Count) || 0; + const value = Number(client[valueKey]) || 0; if (topIds.has(client.AppId)) { - row[client.AppId] += count; + row[client.AppId] += value; } else { - other += count; + other += value; } } if (other > 0) row.Other = other; @@ -86,6 +103,8 @@ export const buildRequestSeries = (buckets, topN = 4) => { return { data, series: topClients.map((c) => ({ AppId: c.AppId, AppName: c.AppName || c.AppId })) }; }; +export const buildRequestSeries = (buckets, topN = 4) => buildStackedSeries(buckets, { topN }); + // Human-readable byte size, one decimal place, B/KB/MB/GB. export const formatBytes = (bytes) => { const n = Number(bytes) || 0; diff --git a/tests/utils/instance-diagnostics.test.js b/tests/utils/instance-diagnostics.test.js index ff66982d8f94..03abb2d3556a 100644 --- a/tests/utils/instance-diagnostics.test.js +++ b/tests/utils/instance-diagnostics.test.js @@ -3,6 +3,7 @@ import { sortDiagnosticsChecks, buildClientLogQuery, buildRequestSeries, + buildStackedSeries, getCheckLabel, formatBytes, } from '../../src/utils/instance-diagnostics' @@ -80,6 +81,49 @@ describe('instance-diagnostics', () => { }) }) + describe('buildStackedSeries', () => { + const egressBuckets = [ + { + BucketStart: '2026-09-10T10:00:00Z', + Clients: [ + { AppId: 'a', AppName: 'App A', Bytes: 100, Requests: 2 }, + { AppId: 'b', AppName: 'App B', Bytes: 300, Requests: 1 }, + { AppId: 'z', AppName: 'App Z', Bytes: 50, Requests: 1 }, + ], + }, + { + BucketStart: '2026-09-10T10:15:00Z', + Clients: [{ AppId: 'b', AppName: 'App B', Bytes: 200, Requests: 3 }], + }, + ] + + it('stacks a chosen value keyed on a chosen bucket field', () => { + const { data, series } = buildStackedSeries(egressBuckets, { + bucketKey: 'BucketStart', + valueKey: 'Bytes', + topN: 2, + }) + expect(series).toEqual([ + { AppId: 'b', AppName: 'App B' }, + { AppId: 'a', AppName: 'App A' }, + ]) + expect(data).toEqual([ + { Bucket: '2026-09-10T10:00:00Z', a: 100, b: 300, Other: 50 }, + { Bucket: '2026-09-10T10:15:00Z', a: 0, b: 200 }, + ]) + }) + + it('keeps the caller order first so a client gets the same colour in both strips', () => { + const { series } = buildStackedSeries(egressBuckets, { + bucketKey: 'BucketStart', + valueKey: 'Bytes', + topN: 2, + order: ['a', 'missing', 'b'], + }) + expect(series.map((s) => s.AppId)).toEqual(['a', 'b']) + }) + }) + describe('sortDiagnosticsChecks', () => { it('orders FAIL, WARN, INFO, PASS', () => { const checks = [ diff --git a/yarn.lock b/yarn.lock index 5670c222f535..e1e475028259 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1947,10 +1947,10 @@ dependencies: "@tybys/wasm-util" "^0.10.3" -"@next/env@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/env/-/env-16.2.11.tgz#9dea1a225a99b1636e5a7166237db1f979b6c532" - integrity sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA== +"@next/env@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/env/-/env-16.3.5.tgz#32fd0528a0deb720c139d6d25737b657e6ba57a3" + integrity sha512-NWEXVDMqoEo0ktmU6u0sE2Vg0LOcsD7NnOTJNo3/fEaTfsg+F1bMIxuDmQbda4e3yTIQwVdUREF2yIuMOusKtg== "@next/eslint-plugin-next@16.3.4": version "16.3.4" @@ -1960,45 +1960,45 @@ "@eslint-community/eslint-utils" "4.9.1" fast-glob "3.3.1" -"@next/swc-darwin-arm64@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.11.tgz#dddeb7795d321321d2b2f01e813b28df22794def" - integrity sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw== - -"@next/swc-darwin-x64@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.11.tgz#bdf0a4d6b36a3ce72c6c997868e76d3bc02043fb" - integrity sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg== - -"@next/swc-linux-arm64-gnu@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.11.tgz#e194f75343aeaa696dc4946c29407909d759d214" - integrity sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g== - -"@next/swc-linux-arm64-musl@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.11.tgz#1ce77d8d30e854b4ef41fa9e24310e411e1a0f96" - integrity sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg== - -"@next/swc-linux-x64-gnu@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.11.tgz#59324ea909a2654b57675cfd6b5fa43f81e05405" - integrity sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w== - -"@next/swc-linux-x64-musl@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.11.tgz#648322d29c955d3ccd78339436f695da5565e366" - integrity sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw== - -"@next/swc-win32-arm64-msvc@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.11.tgz#0c7efca14c2197e8386eb71b6272e237a9a99ac3" - integrity sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g== - -"@next/swc-win32-x64-msvc@16.2.11": - version "16.2.11" - resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.11.tgz#9dcb67b2b2b7e01b68f337958b6c77a973d3b46b" - integrity sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w== +"@next/swc-darwin-arm64@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.5.tgz#b26702b62cb8c90f481d9afddff8e78a33f2d322" + integrity sha512-pMmGgETfKvElucLHtVaeiMRbp2zUbvKx7b1yGko0liBz3cw1mKSggWN/Rp/wPz8z+E1O82u3r4L1Co+ZS5hokQ== + +"@next/swc-darwin-x64@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.5.tgz#ed3a7b1f4ab757f03d306f21d187b951107c4940" + integrity sha512-76VaGYvf6HPa5/w12yLkE3dXTn9AfdEviI79oEL3aZoAmRLc9rWitjWqyjViVysK/ht/y9YKzFkBrUdi/wGkow== + +"@next/swc-linux-arm64-gnu@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.5.tgz#c116c0edc9da69e286b1cf1fc7d55601de1f7362" + integrity sha512-zKDELJ5jSQMHeO/hmXUQsAzagX4bQD4OiMi3pQ5FbUj+yK506oLVHnKA2YXMlbg1EHHqJYtyePOgByIDXD1lqw== + +"@next/swc-linux-arm64-musl@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.5.tgz#dc9902a8c4bec1ba3d3a6af294e288f54c12e34c" + integrity sha512-7Vql0pgzCoHagv6+FNOZoqmJqA52c6zeVbhtS/47qFozO1MSx4ms7x7GHiciY8R5CDsSMKMQjJEryoJLcsBIbA== + +"@next/swc-linux-x64-gnu@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.5.tgz#c092cb89504ac782edb42b253195a73dcfaf2251" + integrity sha512-NH/xzehyHEFWE2nlcZon7TB/0+H4shfWCi7S1zka815XCOhJDYZhoeJtOYy0dh0WVRWACVXSyGNFFytoMxUhRg== + +"@next/swc-linux-x64-musl@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.5.tgz#22e553dee247026244d486d311efd07e4986f9af" + integrity sha512-lV4+EhWMfS8jcC+EH2nn/Cm5cn6XsgbE07bU9tMH8fCo0tNAqhyzi1b5wQ/Tn6NGFTvKDY65w3ZH95EjwBRAnQ== + +"@next/swc-win32-arm64-msvc@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.5.tgz#22fb04be07b602f475b7e4ec7e4b3940551ff749" + integrity sha512-/wKzAREX2RF++MhicjDbg8tGn2AiBIM0+EFeTFKoUEUbW5D6amCJehd5Z5G1H5/gxNdgnwoXMcHz24H/c2tGkQ== + +"@next/swc-win32-x64-msvc@16.3.5": + version "16.3.5" + resolved "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.5.tgz#e137de36cddf64742f739ccb9092c4bcd48d2683" + integrity sha512-LNdCHzgLFc+UeqMS84LzXPaeBRKyqDN9OMyFAr1OrB0XrNw78IRrEVtZvvA7245W/HsaoeVOQX9jPjPk8jojwA== "@nivo/colors@0.99.0": version "0.99.0" @@ -2915,10 +2915,10 @@ "@svgr/plugin-jsx" "8.1.0" "@svgr/plugin-svgo" "8.1.0" -"@swc/helpers@0.5.15": - version "0.5.15" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" - integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== +"@swc/helpers@0.5.23": + version "0.5.23" + resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz#19287d0d86d962b111376039a50c792902c9a86a" + integrity sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw== dependencies: tslib "^2.8.0" @@ -3774,36 +3774,36 @@ resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== -"@vitest/browser-playwright@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz#39e454b78a66e67b3d53e724e74d2020d79a1db7" - integrity sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw== +"@vitest/browser-playwright@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.11.tgz#a49fa8f71d3528645217876a790fc9a9b2cbbb84" + integrity sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg== dependencies: - "@vitest/browser" "4.1.10" - "@vitest/mocker" "4.1.10" + "@vitest/browser" "4.1.11" + "@vitest/mocker" "4.1.11" tinyrainbow "^3.1.0" -"@vitest/browser@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.10.tgz#39f170dc2223fab6209feff706d8939e93051da3" - integrity sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng== +"@vitest/browser@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.11.tgz#ccf94b82d4eaedba929a4290a092f16d55b89afb" + integrity sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w== dependencies: "@blazediff/core" "1.9.1" - "@vitest/mocker" "4.1.10" - "@vitest/utils" "4.1.10" + "@vitest/mocker" "4.1.11" + "@vitest/utils" "4.1.11" magic-string "^0.30.21" pngjs "^7.0.0" sirv "^3.0.2" tinyrainbow "^3.1.0" ws "^8.19.0" -"@vitest/coverage-v8@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz#037cd5e7ea8a2f448f4c2e10db1411c2b0c927bd" - integrity sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g== +"@vitest/coverage-v8@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz#6f0636abe7e23e86dd35127244554be2c60bc2a5" + integrity sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw== dependencies: "@bcoe/v8-coverage" "^1.0.2" - "@vitest/utils" "4.1.10" + "@vitest/utils" "4.1.11" ast-v8-to-istanbul "^1.0.0" istanbul-lib-coverage "^3.2.2" istanbul-lib-report "^3.0.1" @@ -3824,24 +3824,24 @@ chai "^5.2.0" tinyrainbow "^2.0.0" -"@vitest/expect@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz#799c06fc44bb0cf7e2784137b627c5cc173285d4" - integrity sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA== +"@vitest/expect@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz#5f580d1f9cdbba314dbf23b2d911f8eb23878f5f" + integrity sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw== dependencies: "@standard-schema/spec" "^1.1.0" "@types/chai" "^5.2.2" - "@vitest/spy" "4.1.10" - "@vitest/utils" "4.1.10" + "@vitest/spy" "4.1.11" + "@vitest/utils" "4.1.11" chai "^6.2.2" tinyrainbow "^3.1.0" -"@vitest/mocker@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz#2413987ab4cd7fa1c2b614b404c407bf6ad1ead1" - integrity sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow== +"@vitest/mocker@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz#8e2906361bc5dfa271757a858ae80643118fcbb4" + integrity sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ== dependencies: - "@vitest/spy" "4.1.10" + "@vitest/spy" "4.1.11" estree-walker "^3.0.3" magic-string "^0.30.21" @@ -3852,28 +3852,28 @@ dependencies: tinyrainbow "^2.0.0" -"@vitest/pretty-format@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz#75542e7273a08cc10fd4d8dad4e3eb1f16cd958c" - integrity sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q== +"@vitest/pretty-format@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz#8b28eb8240771d6ea970e33beaeb41384b51868e" + integrity sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw== dependencies: tinyrainbow "^3.1.0" -"@vitest/runner@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz#febf0a21a9168421422d1955370e606feab60355" - integrity sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg== +"@vitest/runner@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz#bfbad98c8d6c3f1fb4df12056ad569821ff77f21" + integrity sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw== dependencies: - "@vitest/utils" "4.1.10" + "@vitest/utils" "4.1.11" pathe "^2.0.3" -"@vitest/snapshot@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz#7e3e9fec7d4d47232e493cfdcbd2170de4371c04" - integrity sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw== +"@vitest/snapshot@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz#df461eb165924a3155986dde68e13360f53f3d4c" + integrity sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog== dependencies: - "@vitest/pretty-format" "4.1.10" - "@vitest/utils" "4.1.10" + "@vitest/pretty-format" "4.1.11" + "@vitest/utils" "4.1.11" magic-string "^0.30.21" pathe "^2.0.3" @@ -3884,10 +3884,10 @@ dependencies: tinyspy "^4.0.3" -"@vitest/spy@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz#5c0bfa97b56bba9e37403c976db776ff6ab56f65" - integrity sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw== +"@vitest/spy@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz#0add45cae953afed9c88f98e2f6fc9164558c32a" + integrity sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA== "@vitest/utils@3.2.4": version "3.2.4" @@ -3898,12 +3898,12 @@ loupe "^3.1.4" tinyrainbow "^2.0.0" -"@vitest/utils@4.1.10": - version "4.1.10" - resolved "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz#ffc71055f18bfccb1fd0586365ebc2824892e403" - integrity sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA== +"@vitest/utils@4.1.11": + version "4.1.11" + resolved "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz#9b27a4293b827942b223539bfab1bd9f7eada31b" + integrity sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ== dependencies: - "@vitest/pretty-format" "4.1.10" + "@vitest/pretty-format" "4.1.11" convert-source-map "^2.0.0" tinyrainbow "^3.1.0" @@ -4235,9 +4235,9 @@ baseline-browser-mapping@^2.11.20: integrity sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ== baseline-browser-mapping@^2.9.19: - version "2.10.9" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz#8614229add633061c001a0b7c7c85d4b7c44e6ca" - integrity sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg== + version "2.11.23" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.23.tgz#304c980a35de0f460cf12985359d6e11494c1ab4" + integrity sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ== bidi-js@^1.0.2, bidi-js@^1.0.3: version "1.0.3" @@ -7601,26 +7601,26 @@ natural-compare@^1.4.0: integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== next@^16.2.11: - version "16.2.11" - resolved "https://registry.npmjs.org/next/-/next-16.2.11.tgz#6c568ba65a2d19df6169c92db9649e362ce7f5e9" - integrity sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ== + version "16.3.5" + resolved "https://registry.npmjs.org/next/-/next-16.3.5.tgz#4dd96eebb1d966d4d3946397442ccefc1322b479" + integrity sha512-MdtsTgzyfCPRLC6uJ1mN8ao7lyJ4BB0U6Inhnx3gta1UcCIdHK3yxLG0E8OWQteWD8/Q0qb8A5o7wJaL8M9y2w== dependencies: - "@next/env" "16.2.11" - "@swc/helpers" "0.5.15" + "@next/env" "16.3.5" + "@swc/helpers" "0.5.23" baseline-browser-mapping "^2.9.19" caniuse-lite "^1.0.30001579" - postcss "8.4.31" + postcss "8.5.23" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "16.2.11" - "@next/swc-darwin-x64" "16.2.11" - "@next/swc-linux-arm64-gnu" "16.2.11" - "@next/swc-linux-arm64-musl" "16.2.11" - "@next/swc-linux-x64-gnu" "16.2.11" - "@next/swc-linux-x64-musl" "16.2.11" - "@next/swc-win32-arm64-msvc" "16.2.11" - "@next/swc-win32-x64-msvc" "16.2.11" - sharp "^0.34.5" + "@next/swc-darwin-arm64" "16.3.5" + "@next/swc-darwin-x64" "16.3.5" + "@next/swc-linux-arm64-gnu" "16.3.5" + "@next/swc-linux-arm64-musl" "16.3.5" + "@next/swc-linux-x64-gnu" "16.3.5" + "@next/swc-linux-x64-musl" "16.3.5" + "@next/swc-win32-arm64-msvc" "16.3.5" + "@next/swc-win32-x64-msvc" "16.3.5" + sharp "^0.35.4" no-case@^3.0.4: version "3.0.4" @@ -8031,7 +8031,7 @@ postcss-value-parser@^4.1.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.31, postcss@^8.5.23, postcss@^8.5.26: +postcss@8.5.23, postcss@^8.5.23, postcss@^8.5.26: version "8.5.26" resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== @@ -8919,7 +8919,7 @@ set-proto@^1.0.0: es-errors "^1.3.0" es-object-atoms "^1.0.0" -sharp@^0.34.5, sharp@^0.35.0: +sharp@^0.35.0, sharp@^0.35.4: version "0.35.4" resolved "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz#361df3b2959daeb380541289960456c6be0f92de" integrity sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA== @@ -9881,18 +9881,18 @@ vite@8.2.2, "vite@^6.0.0 || ^7.0.0 || ^8.0.0": optionalDependencies: fsevents "~2.3.3" -vitest@4.1.10: - version "4.1.10" - resolved "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz#7e9285efe264b1167050b7a3a7ff34788e1b7afc" - integrity sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw== - dependencies: - "@vitest/expect" "4.1.10" - "@vitest/mocker" "4.1.10" - "@vitest/pretty-format" "4.1.10" - "@vitest/runner" "4.1.10" - "@vitest/snapshot" "4.1.10" - "@vitest/spy" "4.1.10" - "@vitest/utils" "4.1.10" +vitest@4.1.11: + version "4.1.11" + resolved "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz#1653c1521ae917f960d9b21877797c47dfd8bf21" + integrity sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw== + dependencies: + "@vitest/expect" "4.1.11" + "@vitest/mocker" "4.1.11" + "@vitest/pretty-format" "4.1.11" + "@vitest/runner" "4.1.11" + "@vitest/snapshot" "4.1.11" + "@vitest/spy" "4.1.11" + "@vitest/utils" "4.1.11" es-module-lexer "^2.0.0" expect-type "^1.3.0" magic-string "^0.30.21"