Openstatus www.openstatus.dev

fix: status-page deployment (#1334)

authored by

Maximilian Kaske and committed by
GitHub
0d10a1b3 ef54f12d

+397 -1147
+1 -1
.gitignore
··· 28 28 .env.development.local 29 29 .env.test.local 30 30 .env.production.local 31 + .env*.local 31 32 32 33 # turbo 33 34 .turbo ··· 66 67 apps/ingest-worker/.production.vars 67 68 apps/ingest-worker/.dev.vars 68 69 apps/alerting-engine/tmp/ 69 - .env*.local
+7
apps/status-page/package.json
··· 10 10 "tsc": "tsc --noEmit" 11 11 }, 12 12 "dependencies": { 13 + "@auth/core": "0.40.0", 14 + "@auth/drizzle-adapter": "1.10.0", 13 15 "@date-fns/tz": "1.2.0", 14 16 "@date-fns/utc": "2.1.0", 15 17 "@dnd-kit/core": "6.3.1", ··· 59 61 "@radix-ui/react-tabs": "1.1.12", 60 62 "@radix-ui/react-tooltip": "1.2.7", 61 63 "@sentry/nextjs": "8.46.0", 64 + "@stripe/stripe-js": "2.1.6", 62 65 "@tanstack/react-query": "5.81.5", 63 66 "@tanstack/react-table": "8.21.3", 64 67 "@trpc/client": "11.4.4", ··· 72 75 "date-fns": "4.1.0", 73 76 "lucide-react": "0.525.0", 74 77 "next": "15.4.7", 78 + "next-auth": "5.0.0-beta.29", 75 79 "next-themes": "0.4.6", 76 80 "nuqs": "2.4.3", 81 + "random-word-slugs": "0.1.7", 77 82 "react": "19.1.1", 78 83 "react-day-picker": "8.10.1", 79 84 "react-dom": "19.1.1", ··· 84 89 "remark-parse": "11.0.0", 85 90 "remark-rehype": "11.1.2", 86 91 "sonner": "2.0.5", 92 + "stripe": "13.8.0", 87 93 "superjson": "2.2.2", 88 94 "tailwind-merge": "3.3.1", 89 95 "unified": "11.0.5", ··· 91 97 }, 92 98 "devDependencies": { 93 99 "@tailwindcss/postcss": "4.1.11", 100 + "@types/dom-speech-recognition": "0.0.6", 94 101 "@types/node": "24.0.8", 95 102 "@types/react": "19.1.10", 96 103 "@types/react-dom": "19.1.7",
-88
apps/status-page/src/components/chart/chart-bar-uptime-light.tsx
··· 1 - "use client"; 2 - 3 - import { Skeleton } from "@/components/ui/skeleton"; 4 - import { Bar, BarChart, XAxis } from "recharts"; 5 - 6 - import { 7 - type ChartConfig, 8 - ChartContainer, 9 - ChartTooltip, 10 - ChartTooltipContent, 11 - } from "@/components/ui/chart"; 12 - import { mapUptime } from "@/data/metrics.client"; 13 - import { useTRPC } from "@/lib/trpc/client"; 14 - import type { Region } from "@openstatus/db/src/schema/constants"; 15 - import { useQuery } from "@tanstack/react-query"; 16 - // import { startOfDay, subDays } from "date-fns"; 17 - 18 - const chartConfig = { 19 - ok: { 20 - label: "Success", 21 - color: "var(--color-success)", 22 - }, 23 - degraded: { 24 - label: "Degraded", 25 - color: "var(--color-warning)", 26 - }, 27 - error: { 28 - label: "Error", 29 - color: "var(--color-destructive)", 30 - }, 31 - } satisfies ChartConfig; 32 - 33 - export function ChartBarUptimeLight({ 34 - monitorId, 35 - type, 36 - regions, 37 - }: { 38 - monitorId: string; 39 - type: "http" | "tcp"; 40 - regions?: Region[]; 41 - }) { 42 - const trpc = useTRPC(); 43 - 44 - const { data: uptime, isLoading } = useQuery( 45 - trpc.tinybird.uptime.queryOptions({ 46 - interval: 60 * 24, 47 - // fromDate: startOfDay(subDays(new Date(), 7)).toISOString(), // FIXME: 48 - period: "7d", 49 - monitorId, 50 - regions, 51 - type, 52 - }), 53 - ); 54 - 55 - if (isLoading) { 56 - return <Skeleton className=" my-auto h-5 w-full" />; 57 - } 58 - 59 - const refinedUptime = uptime ? mapUptime(uptime) : []; 60 - 61 - if (refinedUptime.length === 0) { 62 - return <span className="text-muted-foreground">-</span>; 63 - } 64 - 65 - return ( 66 - <ChartContainer config={chartConfig} className="h-[28px] w-full"> 67 - <BarChart accessibilityLayer data={refinedUptime} barCategoryGap={1}> 68 - <ChartTooltip 69 - cursor={false} 70 - allowEscapeViewBox={{ x: false, y: true }} 71 - wrapperStyle={{ zIndex: 1 }} 72 - content={<ChartTooltipContent indicator="dot" />} 73 - /> 74 - <Bar dataKey="ok" stackId="a" fill="var(--color-ok)" /> 75 - <Bar dataKey="error" stackId="a" fill="var(--color-error)" /> 76 - <Bar dataKey="degraded" stackId="a" fill="var(--color-degraded)" /> 77 - <XAxis 78 - dataKey="interval" 79 - tickLine={false} 80 - tickMargin={8} 81 - minTickGap={10} 82 - axisLine={false} 83 - hide 84 - /> 85 - </BarChart> 86 - </ChartContainer> 87 - ); 88 - }
+15 -33
apps/status-page/src/components/chart/chart-tooltip-number.tsx
··· 1 1 import type { ChartConfig } from "@/components/ui/chart"; 2 - import { cn } from "@/lib/utils"; 3 2 import type { 4 3 NameType, 5 4 ValueType, ··· 9 8 chartConfig: ChartConfig; 10 9 value: ValueType; 11 10 name: NameType; 11 + labelFormatter?: (value: ValueType, name: NameType) => React.ReactNode; 12 12 } 13 13 14 14 export function ChartTooltipNumber({ 15 15 value, 16 16 name, 17 17 chartConfig, 18 + labelFormatter, 18 19 }: ChartTooltipNumberProps) { 19 20 return ( 20 - <ChartTooltipNumberRaw 21 - value={value} 22 - label={chartConfig[name as keyof typeof chartConfig]?.label || name} 23 - style={ 24 - { 25 - "--color-bg": `var(--color-${name})`, 26 - } as React.CSSProperties 27 - } 28 - /> 29 - ); 30 - } 31 - 32 - export function ChartTooltipNumberRaw({ 33 - value, 34 - label, 35 - style, 36 - className, 37 - }: { 38 - value: ValueType; 39 - label: React.ReactNode; 40 - style?: React.CSSProperties; 41 - className?: string; 42 - }) { 43 - return ( 44 21 <> 45 22 <div 46 - className={cn( 47 - "h-2.5 w-2.5 shrink-0 rounded-[2px] bg-(--color-bg)", 48 - className, 49 - )} 50 - style={style} 23 + className="h-2.5 w-2.5 shrink-0 rounded-[2px] bg-(--color-bg)" 24 + style={ 25 + { 26 + "--color-bg": `var(--color-${name})`, 27 + } as React.CSSProperties 28 + } 51 29 /> 52 - <span>{label}</span> 53 - <div className="ml-auto flex items-baseline gap-0.5 font-medium font-mono text-foreground tabular-nums"> 30 + <span> 31 + {labelFormatter 32 + ? labelFormatter(value, name) 33 + : chartConfig[name as keyof typeof chartConfig]?.label || name} 34 + </span> 35 + <div className="text-foreground ml-auto flex items-baseline gap-0.5 font-mono font-medium tabular-nums"> 54 36 {value} 55 - <span className="font-normal text-muted-foreground">ms</span> 37 + <span className="text-muted-foreground font-normal">ms</span> 56 38 </div> 57 39 </> 58 40 );
+7 -26
apps/status-page/src/components/content/metric-card.tsx
··· 3 3 import { ChevronDown, ChevronUp } from "lucide-react"; 4 4 5 5 import { Badge } from "@/components/ui/badge"; 6 - import { Skeleton } from "@/components/ui/skeleton"; 7 6 8 7 import { cn } from "@/lib/utils"; 9 - import type React from "react"; 10 8 11 9 const metricCardVariants = cva( 12 10 "flex flex-col gap-1 border rounded-lg px-3 py-2 text-card-foreground", ··· 35 33 return ( 36 34 <div 37 35 data-variant={variant} 38 - className={cn(metricCardVariants({ variant, className }), "group")} 36 + className={cn(metricCardVariants({ variant, className }), "group/metric")} 39 37 {...props} 40 38 > 41 39 {children} ··· 49 47 ...props 50 48 }: React.ComponentProps<"p">) { 51 49 return ( 52 - <p className={cn("font-medium text-sm", className)} {...props}> 50 + <p className={cn("text-sm font-medium", className)} {...props}> 53 51 {children} 54 52 </p> 55 53 ); ··· 64 62 <div 65 63 className={cn( 66 64 "text-muted-foreground", 67 - "group-data-[variant=destructive]:text-destructive", 68 - "group-data-[variant=success]:text-success", 69 - "group-data-[variant=warning]:text-warning", 65 + "group-data-[variant=destructive]/metric:text-destructive", 66 + "group-data-[variant=success]/metric:text-success", 67 + "group-data-[variant=warning]/metric:text-warning", 70 68 className, 71 69 )} 72 70 {...props} ··· 82 80 ...props 83 81 }: React.ComponentProps<"p">) { 84 82 return ( 85 - <p className={cn("font-semibold text-foreground", className)} {...props}> 83 + <p className={cn("text-foreground font-semibold", className)} {...props}> 86 84 {children} 87 85 </p> 88 86 ); ··· 96 94 return ( 97 95 <div 98 96 className={cn( 99 - "grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5", 97 + "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4", 100 98 className, 101 99 )} 102 100 {...props} ··· 178 176 </button> 179 177 ); 180 178 } 181 - 182 - export function MetricCardSkeleton({ 183 - className, 184 - ...props 185 - }: React.ComponentProps<typeof Skeleton>) { 186 - return ( 187 - <Skeleton 188 - className={cn( 189 - "group-data-[variant=destructive]:bg-destructive/50", 190 - "group-data-[variant=success]:bg-success/50", 191 - "group-data-[variant=warning]:bg-warning/50", 192 - className, 193 - )} 194 - {...props} 195 - /> 196 - ); 197 - }
apps/status-page/src/components/controls-filter/.gitkeep

This is a binary file and will not be displayed.

-45
apps/status-page/src/components/controls-search/button-reset.tsx
··· 1 - "use client"; 2 - 3 - import { Button } from "@/components/ui/button"; 4 - import { X } from "lucide-react"; 5 - import { useRouter, useSearchParams } from "next/navigation"; 6 - 7 - export function ButtonReset({ only }: { only?: string[] }) { 8 - const searchParams = useSearchParams(); 9 - const router = useRouter(); 10 - 11 - // Determine if at least one parameter that should be reset is present (or any parameter if `only` is undefined) 12 - const hasParamsToReset = only 13 - ? only.some((key) => searchParams.has(key)) 14 - : !!searchParams.toString(); 15 - 16 - if (!hasParamsToReset) return null; 17 - 18 - const handleClick = () => { 19 - // Clone the current search params so we can mutate them 20 - const params = new URLSearchParams(searchParams.toString()); 21 - 22 - if (only && only.length > 0) { 23 - // Remove only the specified keys 24 - only.forEach((key) => params.delete(key)); 25 - const query = params.toString(); 26 - router.push( 27 - query 28 - ? `${window.location.pathname}?${query}` 29 - : window.location.pathname, 30 - ); 31 - } else { 32 - // No `only` prop provided – remove all query parameters 33 - router.push(window.location.pathname); 34 - } 35 - }; 36 - 37 - if (!hasParamsToReset) return null; 38 - 39 - return ( 40 - <Button variant="ghost" size="sm" onClick={handleClick}> 41 - <X /> 42 - Reset 43 - </Button> 44 - ); 45 - }
-163
apps/status-page/src/components/controls-search/command-region.tsx
··· 1 - "use client"; 2 - 3 - import { Link } from "@/components/common/link"; 4 - import { 5 - BillingOverlay, 6 - BillingOverlayButton, 7 - BillingOverlayDescription, 8 - } from "@/components/content/billing-overlay"; 9 - import { Button } from "@/components/ui/button"; 10 - import { 11 - Command, 12 - CommandEmpty, 13 - CommandGroup, 14 - CommandInput, 15 - CommandItem, 16 - CommandList, 17 - CommandSeparator, 18 - } from "@/components/ui/command"; 19 - import { 20 - Popover, 21 - PopoverContent, 22 - PopoverTrigger, 23 - } from "@/components/ui/popover"; 24 - import { REGIONS } from "@/data/metrics.client"; 25 - import { useTRPC } from "@/lib/trpc/client"; 26 - import { cn } from "@/lib/utils"; 27 - import { groupByContinent } from "@openstatus/utils"; 28 - import { useQuery } from "@tanstack/react-query"; 29 - import { Check, Lock } from "lucide-react"; 30 - import { parseAsArrayOf, parseAsStringLiteral, useQueryState } from "nuqs"; 31 - 32 - export const parseRegions = (regions: (typeof REGIONS)[number][]) => 33 - parseAsArrayOf( 34 - parseAsStringLiteral(REGIONS.filter((region) => regions.includes(region))), 35 - ).withDefault(regions as unknown as (typeof REGIONS)[number][]); 36 - 37 - export function CommandRegion({ 38 - regions, 39 - }: { 40 - regions: (typeof REGIONS)[number][]; 41 - }) { 42 - const trpc = useTRPC(); 43 - const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); 44 - const [selectedRegions, setSelectedRegions] = useQueryState( 45 - "regions", 46 - parseRegions(regions), 47 - ); 48 - 49 - const limited = workspace?.plan === "free"; 50 - 51 - return ( 52 - <Popover> 53 - <PopoverTrigger asChild> 54 - <Button variant="outline" size="sm"> 55 - {selectedRegions.length === regions.length 56 - ? "All Regions" 57 - : `${selectedRegions.length} Regions`} 58 - </Button> 59 - </PopoverTrigger> 60 - <PopoverContent 61 - align="start" 62 - className="relative w-[200px] overflow-hidden p-0" 63 - > 64 - <Command> 65 - <CommandInput placeholder="Search region..." disabled={limited} /> 66 - <CommandList> 67 - <CommandGroup forceMount> 68 - <CommandItem 69 - onSelect={() => { 70 - const items = document.querySelectorAll( 71 - '[data-slot="command-item"][data-disabled="false"]', 72 - ); 73 - const codes: (typeof REGIONS)[number][] = []; 74 - 75 - items.forEach((item) => { 76 - const code = item.getAttribute("data-value"); 77 - if (code && code !== "select-all") { 78 - codes.push(code as (typeof REGIONS)[number]); 79 - } 80 - }); 81 - 82 - if (codes.length === selectedRegions.length) { 83 - setSelectedRegions([]); 84 - } else { 85 - setSelectedRegions(codes); 86 - } 87 - }} 88 - value="select-all" 89 - disabled={limited} 90 - > 91 - Toggle selection 92 - </CommandItem> 93 - </CommandGroup> 94 - <CommandSeparator alwaysRender /> 95 - {Object.entries(groupByContinent).map( 96 - ([continent, continentRegions]) => { 97 - const allowedRegions = continentRegions.filter((region) => 98 - regions.includes(region.code), 99 - ); 100 - 101 - if (allowedRegions.length === 0) { 102 - return null; 103 - } 104 - return ( 105 - <CommandGroup key={continent} heading={continent}> 106 - {allowedRegions.map((region) => ( 107 - <CommandItem 108 - disabled={limited} 109 - key={region.code} 110 - value={region.code} 111 - keywords={[ 112 - region.code, 113 - region.location, 114 - region.continent, 115 - region.flag, 116 - ]} 117 - onSelect={() => { 118 - setSelectedRegions((prev) => 119 - prev.includes(region.code) 120 - ? prev.filter((r) => r !== region.code) 121 - : [...prev, region.code], 122 - ); 123 - }} 124 - > 125 - <span className="mr-1">{region.flag}</span> 126 - {region.code} 127 - <span className="ml-1 truncate text-muted-foreground text-xs"> 128 - {region.location} 129 - </span> 130 - <Check 131 - className={cn( 132 - "ml-auto", 133 - selectedRegions.includes(region.code) 134 - ? "opacity-100" 135 - : "opacity-0", 136 - )} 137 - /> 138 - </CommandItem> 139 - ))} 140 - </CommandGroup> 141 - ); 142 - }, 143 - )} 144 - <CommandEmpty>No region found.</CommandEmpty> 145 - </CommandList> 146 - </Command> 147 - {limited ? ( 148 - <BillingOverlay className="to-70%"> 149 - <BillingOverlayButton asChild> 150 - <Link href="/settings/billing"> 151 - <Lock /> 152 - Upgrade 153 - </Link> 154 - </BillingOverlayButton> 155 - <BillingOverlayDescription> 156 - Filter by region is only available on paid plans. 157 - </BillingOverlayDescription> 158 - </BillingOverlay> 159 - ) : null} 160 - </PopoverContent> 161 - </Popover> 162 - ); 163 - }
-87
apps/status-page/src/components/controls-search/command-tags.tsx
··· 1 - "use client"; 2 - 3 - import { Button } from "@/components/ui/button"; 4 - import { 5 - Command, 6 - CommandEmpty, 7 - CommandGroup, 8 - CommandInput, 9 - CommandItem, 10 - CommandList, 11 - } from "@/components/ui/command"; 12 - import { 13 - Popover, 14 - PopoverContent, 15 - PopoverTrigger, 16 - } from "@/components/ui/popover"; 17 - import { useTRPC } from "@/lib/trpc/client"; 18 - import { cn } from "@/lib/utils"; 19 - import { useQuery } from "@tanstack/react-query"; 20 - import { Check } from "lucide-react"; 21 - import { parseAsArrayOf, parseAsString, useQueryState } from "nuqs"; 22 - 23 - export function CommandTags() { 24 - const trpc = useTRPC(); 25 - const { data: tags } = useQuery(trpc.monitorTag.list.queryOptions()); 26 - const [selectedTags, setSelectedTags] = useQueryState( 27 - "tags", 28 - parseAsArrayOf(parseAsString).withDefault([]).withOptions({ 29 - shallow: false, 30 - }), 31 - ); 32 - 33 - return ( 34 - <Popover> 35 - <PopoverTrigger asChild> 36 - <Button variant="outline" size="sm"> 37 - {selectedTags.length === (tags?.length ?? 0) 38 - ? "All Tags" 39 - : `${selectedTags.length} Tags`} 40 - </Button> 41 - </PopoverTrigger> 42 - <PopoverContent 43 - align="start" 44 - className="relative w-[200px] overflow-hidden p-0" 45 - > 46 - <Command> 47 - <CommandInput placeholder="Search tag..." /> 48 - <CommandList> 49 - <CommandGroup> 50 - {tags?.map((tag) => ( 51 - <CommandItem 52 - key={tag.id} 53 - value={tag.name} 54 - keywords={[tag.name]} 55 - onSelect={() => { 56 - setSelectedTags((prev) => 57 - prev.includes(tag.name) 58 - ? prev.filter((r) => r !== tag.name) 59 - : [...prev, tag.name], 60 - ); 61 - }} 62 - > 63 - <div className="flex items-center gap-2"> 64 - <span 65 - className="size-2.5 rounded-full" 66 - style={{ backgroundColor: tag.color }} 67 - /> 68 - {tag.name} 69 - </div> 70 - <Check 71 - className={cn( 72 - "ml-auto", 73 - selectedTags.includes(tag.name) 74 - ? "opacity-100" 75 - : "opacity-0", 76 - )} 77 - /> 78 - </CommandItem> 79 - ))} 80 - </CommandGroup> 81 - <CommandEmpty>No tag found.</CommandEmpty> 82 - </CommandList> 83 - </Command> 84 - </PopoverContent> 85 - </Popover> 86 - ); 87 - }
-56
apps/status-page/src/components/controls-search/dropdown-interval.tsx
··· 1 - "use client"; 2 - 3 - import { Button } from "@/components/ui/button"; 4 - import { 5 - DropdownMenu, 6 - DropdownMenuContent, 7 - DropdownMenuGroup, 8 - DropdownMenuItem, 9 - DropdownMenuLabel, 10 - DropdownMenuTrigger, 11 - } from "@/components/ui/dropdown-menu"; 12 - import { INTERVALS } from "@/data/metrics.client"; 13 - import { Check } from "lucide-react"; 14 - import { parseAsNumberLiteral, useQueryState } from "nuqs"; 15 - 16 - const MAPPING = { 17 - 5: "5 minutes", 18 - 15: "15 minutes", 19 - 30: "30 minutes", 20 - 60: "1 hour", 21 - 120: "2 hours", 22 - 240: "4 hours", 23 - 480: "8 hours", 24 - 1440: "1 day", 25 - } as const; 26 - 27 - const parseInterval = parseAsNumberLiteral(INTERVALS).withDefault(30); 28 - 29 - export function DropdownInterval() { 30 - const [interval, setInterval] = useQueryState("interval", parseInterval); 31 - 32 - return ( 33 - <DropdownMenu> 34 - <DropdownMenuTrigger asChild> 35 - <Button variant="outline" size="sm"> 36 - {MAPPING[interval]} 37 - </Button> 38 - </DropdownMenuTrigger> 39 - <DropdownMenuContent align="start"> 40 - <DropdownMenuGroup> 41 - <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 42 - Resolution 43 - </DropdownMenuLabel> 44 - {INTERVALS.map((item) => ( 45 - <DropdownMenuItem key={item} onSelect={() => setInterval(item)}> 46 - {MAPPING[item]} 47 - {interval === item ? ( 48 - <Check className="ml-auto shrink-0" /> 49 - ) : null} 50 - </DropdownMenuItem> 51 - ))} 52 - </DropdownMenuGroup> 53 - </DropdownMenuContent> 54 - </DropdownMenu> 55 - ); 56 - }
-53
apps/status-page/src/components/controls-search/dropdown-percentile.tsx
··· 1 - "use client"; 2 - 3 - import { Button } from "@/components/ui/button"; 4 - import { 5 - DropdownMenu, 6 - DropdownMenuContent, 7 - DropdownMenuGroup, 8 - DropdownMenuItem, 9 - DropdownMenuLabel, 10 - DropdownMenuTrigger, 11 - } from "@/components/ui/dropdown-menu"; 12 - import { PERCENTILES } from "@/data/metrics.client"; 13 - import { cn } from "@/lib/utils"; 14 - import { Check } from "lucide-react"; 15 - import { parseAsStringLiteral, useQueryState } from "nuqs"; 16 - 17 - const parsePercentile = parseAsStringLiteral(PERCENTILES).withDefault("p50"); 18 - 19 - export function DropdownPercentile() { 20 - const [percentile, setPercentile] = useQueryState( 21 - "percentile", 22 - parsePercentile, 23 - ); 24 - 25 - return ( 26 - <DropdownMenu> 27 - <DropdownMenuTrigger asChild> 28 - <Button variant="outline" size="sm" className="capitalize"> 29 - {percentile} 30 - </Button> 31 - </DropdownMenuTrigger> 32 - <DropdownMenuContent align="start"> 33 - <DropdownMenuGroup> 34 - <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 35 - Percentile 36 - </DropdownMenuLabel> 37 - {PERCENTILES.map((item) => ( 38 - <DropdownMenuItem 39 - key={item} 40 - onSelect={() => setPercentile(item)} 41 - className={cn("capitalize")} 42 - > 43 - {item} 44 - {percentile === item ? ( 45 - <Check className="ml-auto shrink-0" /> 46 - ) : null} 47 - </DropdownMenuItem> 48 - ))} 49 - </DropdownMenuGroup> 50 - </DropdownMenuContent> 51 - </DropdownMenu> 52 - ); 53 - }
-59
apps/status-page/src/components/controls-search/dropdown-period.tsx
··· 1 - "use client"; 2 - 3 - import { Button } from "@/components/ui/button"; 4 - import { 5 - DropdownMenu, 6 - DropdownMenuContent, 7 - DropdownMenuGroup, 8 - DropdownMenuItem, 9 - DropdownMenuLabel, 10 - DropdownMenuTrigger, 11 - } from "@/components/ui/dropdown-menu"; 12 - import { PERIODS } from "@/data/metrics.client"; 13 - import { Check } from "lucide-react"; 14 - import { parseAsStringLiteral, useQueryState } from "nuqs"; 15 - 16 - // TODO: where to move it? 17 - export const PERIOD_VALUES = [ 18 - { 19 - value: "1d", 20 - label: "Last day", 21 - }, 22 - { 23 - value: "7d", 24 - label: "Last 7 days", 25 - }, 26 - { 27 - value: "14d", 28 - label: "Last 14 days", 29 - }, 30 - ] satisfies { value: (typeof PERIODS)[number]; label: string }[]; 31 - 32 - const parsePeriod = parseAsStringLiteral(PERIODS).withDefault("1d"); 33 - 34 - export function DropdownPeriod() { 35 - const [period, setPeriod] = useQueryState("period", parsePeriod); 36 - 37 - return ( 38 - <DropdownMenu> 39 - <DropdownMenuTrigger asChild> 40 - <Button variant="outline" size="sm"> 41 - {PERIOD_VALUES.find(({ value }) => value === period)?.label} 42 - </Button> 43 - </DropdownMenuTrigger> 44 - <DropdownMenuContent align="start"> 45 - <DropdownMenuGroup> 46 - <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 47 - Period 48 - </DropdownMenuLabel> 49 - {PERIOD_VALUES.map(({ value, label }) => ( 50 - <DropdownMenuItem key={value} onSelect={() => setPeriod(value)}> 51 - {label} 52 - {period === value ? <Check className="ml-auto shrink-0" /> : null} 53 - </DropdownMenuItem> 54 - ))} 55 - </DropdownMenuGroup> 56 - </DropdownMenuContent> 57 - </DropdownMenu> 58 - ); 59 - }
-48
apps/status-page/src/components/controls-search/dropdown-status.tsx
··· 1 - "use client"; 2 - 3 - import { Button } from "@/components/ui/button"; 4 - import { 5 - DropdownMenu, 6 - DropdownMenuContent, 7 - DropdownMenuGroup, 8 - DropdownMenuItem, 9 - DropdownMenuLabel, 10 - DropdownMenuTrigger, 11 - } from "@/components/ui/dropdown-menu"; 12 - import { STATUS } from "@/data/metrics.client"; 13 - import { cn } from "@/lib/utils"; 14 - import { Check } from "lucide-react"; 15 - import { parseAsStringLiteral, useQueryState } from "nuqs"; 16 - 17 - const parseStatus = parseAsStringLiteral(STATUS); 18 - 19 - export function DropdownStatus() { 20 - const [status, setStatus] = useQueryState("status", parseStatus); 21 - 22 - return ( 23 - <DropdownMenu> 24 - <DropdownMenuTrigger asChild> 25 - <Button variant="outline" size="sm" className="capitalize"> 26 - {status ?? "All Status"} 27 - </Button> 28 - </DropdownMenuTrigger> 29 - <DropdownMenuContent align="start"> 30 - <DropdownMenuGroup> 31 - <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 32 - Request Status 33 - </DropdownMenuLabel> 34 - {STATUS.map((item) => ( 35 - <DropdownMenuItem 36 - key={item} 37 - onSelect={() => setStatus(item)} 38 - className={cn("capitalize")} 39 - > 40 - {item} 41 - {status === item ? <Check className="ml-auto shrink-0" /> : null} 42 - </DropdownMenuItem> 43 - ))} 44 - </DropdownMenuGroup> 45 - </DropdownMenuContent> 46 - </DropdownMenu> 47 - ); 48 - }
-48
apps/status-page/src/components/controls-search/dropdown-trigger.tsx
··· 1 - "use client"; 2 - 3 - import { Button } from "@/components/ui/button"; 4 - import { 5 - DropdownMenu, 6 - DropdownMenuContent, 7 - DropdownMenuGroup, 8 - DropdownMenuItem, 9 - DropdownMenuLabel, 10 - DropdownMenuTrigger, 11 - } from "@/components/ui/dropdown-menu"; 12 - import { TRIGGER } from "@/data/metrics.client"; 13 - import { cn } from "@/lib/utils"; 14 - import { Check } from "lucide-react"; 15 - import { parseAsStringLiteral, useQueryState } from "nuqs"; 16 - 17 - const parseTrigger = parseAsStringLiteral(TRIGGER); 18 - 19 - export function DropdownTrigger() { 20 - const [trigger, setTrigger] = useQueryState("trigger", parseTrigger); 21 - 22 - return ( 23 - <DropdownMenu> 24 - <DropdownMenuTrigger asChild> 25 - <Button variant="outline" size="sm" className="capitalize"> 26 - {trigger ?? "All Trigger"} 27 - </Button> 28 - </DropdownMenuTrigger> 29 - <DropdownMenuContent align="start"> 30 - <DropdownMenuGroup> 31 - <DropdownMenuLabel className="font-medium text-muted-foreground text-xs"> 32 - Trigger 33 - </DropdownMenuLabel> 34 - {TRIGGER.map((item) => ( 35 - <DropdownMenuItem 36 - key={item} 37 - onSelect={() => setTrigger(item)} 38 - className={cn("capitalize")} 39 - > 40 - {item === "cron" ? "Scheduled" : "API"} 41 - {trigger === item ? <Check className="ml-auto shrink-0" /> : null} 42 - </DropdownMenuItem> 43 - ))} 44 - </DropdownMenuGroup> 45 - </DropdownMenuContent> 46 - </DropdownMenu> 47 - ); 48 - }
-141
apps/status-page/src/components/controls-search/popover-date.tsx
··· 1 - import { DatePicker } from "@/components/date-picker"; 2 - import { Button } from "@/components/ui/button"; 3 - import { 4 - Popover, 5 - PopoverContent, 6 - PopoverTrigger, 7 - } from "@/components/ui/popover"; 8 - import { formatDateRange } from "@/lib/formatter"; 9 - import { endOfDay, startOfDay, subDays, subHours } from "date-fns"; 10 - import { parseAsIsoDateTime, useQueryState } from "nuqs"; 11 - import { useEffect, useMemo, useRef, useState } from "react"; 12 - import type { DateRange } from "react-day-picker"; 13 - 14 - export function PopoverDate() { 15 - const [open, setOpen] = useState(false); 16 - const today = useRef(new Date()); 17 - const [from, setFrom] = useQueryState( 18 - "from", 19 - parseAsIsoDateTime.withDefault(startOfDay(today.current)), 20 - ); 21 - const [to, setTo] = useQueryState( 22 - "to", 23 - parseAsIsoDateTime.withDefault(endOfDay(today.current)), 24 - ); 25 - const [range, setRange] = useState<DateRange>({ from, to }); 26 - 27 - // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 28 - const presets = useMemo( 29 - () => [ 30 - { 31 - id: "today", 32 - label: "Today", 33 - values: { 34 - from: startOfDay(today.current), 35 - to: endOfDay(today.current), 36 - }, 37 - shortcut: "t", 38 - }, 39 - { 40 - id: "yesterday", 41 - label: "Yesterday", 42 - values: { 43 - from: startOfDay(subDays(today.current, 1)), 44 - to: endOfDay(subDays(today.current, 1)), 45 - }, 46 - shortcut: "y", 47 - }, 48 - { 49 - id: "lastHour", 50 - label: "Last hour", 51 - values: { 52 - from: subHours(today.current, 1), 53 - to: today.current, 54 - }, 55 - shortcut: "h", 56 - }, 57 - { 58 - id: "last6Hours", 59 - label: "Last 6 hours", 60 - values: { 61 - from: subHours(today.current, 5), 62 - to: today.current, 63 - }, 64 - shortcut: "s", 65 - }, 66 - { 67 - id: "last24Hours", 68 - label: "Last 24 hours", 69 - values: { 70 - from: subHours(today.current, 23), 71 - to: today.current, 72 - }, 73 - shortcut: "d", 74 - }, 75 - { 76 - id: "last7Days", 77 - label: "Last 7 days", 78 - values: { 79 - from: subDays(today.current, 6), 80 - to: today.current, 81 - }, 82 - shortcut: "w", 83 - }, 84 - { 85 - id: "last14Days", 86 - label: "Last 14 days", 87 - values: { 88 - from: subDays(today.current, 13), 89 - to: today.current, 90 - }, 91 - shortcut: "b", 92 - }, 93 - ], 94 - [today], 95 - ); 96 - 97 - // instead use `range` state 98 - const selected = presets.find((period) => { 99 - return ( 100 - from.getTime() === period.values.from.getTime() && 101 - to.getTime() === period.values.to.getTime() 102 - ); 103 - }); 104 - 105 - // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 106 - useEffect(() => { 107 - if (!open) { 108 - setFrom(range.from ?? null); 109 - setTo(range.to ?? null); 110 - } 111 - }, [open]); 112 - 113 - useEffect(() => { 114 - const down = (e: KeyboardEvent) => { 115 - if (!open) return; 116 - 117 - presets.map((preset) => { 118 - if (preset.shortcut === e.key) { 119 - setFrom(preset.values.from); 120 - setTo(preset.values.to); 121 - setRange({ from: preset.values.from, to: preset.values.to }); 122 - } 123 - }); 124 - }; 125 - document.addEventListener("keydown", down); 126 - return () => document.removeEventListener("keydown", down); 127 - }, [presets, open, setFrom, setTo]); 128 - 129 - return ( 130 - <Popover open={open} onOpenChange={setOpen}> 131 - <PopoverTrigger asChild> 132 - <Button variant="outline" size="sm"> 133 - {selected?.label ?? formatDateRange(from, to)} 134 - </Button> 135 - </PopoverTrigger> 136 - <PopoverContent className="w-auto p-0" side="bottom" align="start"> 137 - <DatePicker presets={presets} range={range} onSelect={setRange} /> 138 - </PopoverContent> 139 - </Popover> 140 - ); 141 - }
-49
apps/status-page/src/components/development-indicator.tsx
··· 1 - "use client"; 2 - 3 - import { useIsMobile } from "@/hooks/use-mobile"; 4 - import * as Portal from "@radix-ui/react-portal"; 5 - import { Kbd } from "./common/kbd"; 6 - import { 7 - Tooltip, 8 - TooltipContent, 9 - TooltipProvider, 10 - TooltipTrigger, 11 - } from "./ui/tooltip"; 12 - 13 - export function DevelopmentIndicator() { 14 - const isMobile = useIsMobile(); 15 - 16 - if (process.env.NODE_ENV !== "production") return null; 17 - 18 - return ( 19 - <Portal.Root> 20 - <div className="pointer-events-none fixed inset-0 z-[9999] border-2 border-destructive" /> 21 - <div className="fixed inset-x-0 bottom-0 z-[9999] select-none"> 22 - <div className="flex items-center justify-center"> 23 - <TooltipProvider delayDuration={0}> 24 - <Tooltip> 25 - <TooltipTrigger> 26 - <div className="w-fit rounded-t bg-destructive px-2 py-1 font-mono text-background text-xs"> 27 - In Beta 28 - </div> 29 - </TooltipTrigger> 30 - <TooltipContent side="top"> 31 - {!isMobile ? ( 32 - <p> 33 - Press{" "} 34 - <Kbd variant="secondary" className="-me-0 ms-0"> 35 - F 36 - </Kbd>{" "} 37 - key to provide feedback. 38 - </p> 39 - ) : ( 40 - <p>Use a larger screen to provide feedback.</p> 41 - )} 42 - </TooltipContent> 43 - </Tooltip> 44 - </TooltipProvider> 45 - </div> 46 - </div> 47 - </Portal.Root> 48 - ); 49 - }
-40
apps/status-page/src/data/audit-logs.client.ts
··· 1 - import { 2 - CircleAlert, 3 - CircleCheck, 4 - CircleMinus, 5 - Send, 6 - Siren, 7 - } from "lucide-react"; 8 - 9 - export const config = { 10 - "incident.created": { 11 - icon: Siren, 12 - color: "text-destructive", 13 - title: "Incident Created", 14 - }, 15 - "incident.resolved": { 16 - icon: CircleCheck, 17 - color: "text-success", 18 - title: "Incident Resolved", 19 - }, 20 - "monitor.failed": { 21 - icon: CircleMinus, 22 - color: "text-destructive", 23 - title: "Monitor Failed", 24 - }, 25 - "notification.sent": { 26 - icon: Send, 27 - color: "text-info", 28 - title: "Notification Sent", 29 - }, 30 - "monitor.recovered": { 31 - icon: CircleCheck, 32 - color: "text-success", 33 - title: "Monitor Recovered", 34 - }, 35 - "monitor.degraded": { 36 - icon: CircleAlert, 37 - color: "text-warning", 38 - title: "Monitor Degraded", 39 - }, 40 - } as const;
-90
apps/status-page/src/data/audit-logs.ts
··· 1 - export const auditLogs = [ 2 - { 3 - id: 3, 4 - timestamp: new Date("2025-05-05 12:00:00"), 5 - action: "incident.created" as const, 6 - }, 7 - { 8 - id: 2, 9 - timestamp: new Date("2025-05-05 12:00:00"), 10 - action: "monitor.failed" as const, 11 - metadata: { 12 - region: "ams", 13 - status: 500, 14 - latency: 1400, 15 - } as const, 16 - }, 17 - { 18 - id: 1, 19 - timestamp: new Date("2025-05-05 12:00:00"), 20 - action: "notification.sent" as const, 21 - metadata: { 22 - provider: "slack", 23 - } as const, 24 - }, 25 - { 26 - id: 0, 27 - timestamp: new Date("2025-05-05 12:00:00"), 28 - action: "monitor.recovered" as const, 29 - metadata: { 30 - region: "ams", 31 - latency: 140, 32 - } as const, 33 - }, 34 - { 35 - id: -1, 36 - timestamp: new Date("2025-05-05 12:00:00"), 37 - action: "monitor.degraded" as const, 38 - metadata: { 39 - region: "ams", 40 - latency: 30_000, 41 - } as const, 42 - }, 43 - { 44 - id: -2, 45 - timestamp: new Date("2025-05-05 12:00:00"), 46 - action: "incident.resolved" as const, 47 - }, 48 - { 49 - id: -3, 50 - timestamp: new Date("2025-05-05 12:00:00"), 51 - action: "incident.created" as const, 52 - }, 53 - { 54 - id: -4, 55 - timestamp: new Date("2025-05-05 12:00:00"), 56 - action: "monitor.degraded" as const, 57 - metadata: { 58 - region: "ams", 59 - latency: 30_000, 60 - } as const, 61 - }, 62 - { 63 - id: -5, 64 - timestamp: new Date("2025-05-05 12:00:00"), 65 - action: "monitor.degraded" as const, 66 - metadata: { 67 - region: "ams", 68 - latency: 32_000, 69 - } as const, 70 - }, 71 - { 72 - id: -6, 73 - timestamp: new Date("2025-05-05 12:00:00"), 74 - action: "monitor.degraded" as const, 75 - metadata: { 76 - region: "ams", 77 - latency: 33_000, 78 - } as const, 79 - }, 80 - { 81 - id: -7, 82 - timestamp: new Date("2025-05-05 12:00:00"), 83 - action: "monitor.degraded" as const, 84 - metadata: { 85 - region: "ams", 86 - latency: 34_000, 87 - } as const, 88 - }, 89 - ]; 90 - export type AuditLog = (typeof auditLogs)[number];
+325
apps/status-page/src/data/metrics.client.ts
··· 1 + "use client"; 2 + 3 + import type { MetricCard } from "@/components/content/metric-card"; 4 + import { formatDateTime, formatMilliseconds } from "@/lib/formatter"; 5 + import type { RouterOutputs } from "@openstatus/api"; 6 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 7 + import type { RegionMetric } from "./region-metrics"; 8 + import type { Region } from "./regions"; 9 + 10 + export const STATUS = ["success", "error", "degraded"] as const; 11 + export const PERIODS = ["1d", "7d", "14d"] as const; 12 + export const REGIONS = flyRegions as unknown as (typeof flyRegions)[number][]; 13 + export const PERCENTILES = ["p50", "p75", "p90", "p95", "p99"] as const; 14 + export const INTERVALS = [5, 15, 30, 60, 120, 240, 480, 1440] as const; 15 + export const TRIGGER = ["api", "cron"] as const; 16 + 17 + const PERCENTILE_MAP = { 18 + p50: "p50Latency", 19 + p75: "p75Latency", 20 + p90: "p90Latency", 21 + p95: "p95Latency", 22 + p99: "p99Latency", 23 + } as const; 24 + 25 + // FIXME: rename pipe return values 26 + 27 + export function mapMetrics(metrics: RouterOutputs["tinybird"]["metrics"]) { 28 + return metrics.data?.map((metric) => { 29 + return { 30 + p50: metric.p50Latency, 31 + p75: metric.p75Latency, 32 + p90: metric.p90Latency, 33 + p95: metric.p95Latency, 34 + p99: metric.p99Latency, 35 + total: metric.count, 36 + uptime: (metric.success + metric.degraded) / metric.count, 37 + degraded: metric.degraded, 38 + error: metric.error, 39 + lastTimestamp: metric.lastTimestamp, 40 + }; 41 + }); 42 + } 43 + 44 + export const metricsCards = { 45 + uptime: { 46 + label: "UPTIME", 47 + variant: "success", 48 + }, 49 + degraded: { 50 + label: "DEGRADED", 51 + variant: "warning", 52 + }, 53 + error: { 54 + label: "FAILING", 55 + variant: "destructive", 56 + }, 57 + total: { 58 + label: "REQUESTS", 59 + variant: "default", 60 + }, 61 + lastTimestamp: { 62 + label: "LAST CHECKED", 63 + variant: "ghost", 64 + }, 65 + p50: { 66 + label: "P50", 67 + variant: "default", 68 + }, 69 + p75: { 70 + label: "P75", 71 + variant: "default", 72 + }, 73 + p90: { 74 + label: "P90", 75 + variant: "default", 76 + }, 77 + p95: { 78 + label: "P95", 79 + variant: "default", 80 + }, 81 + p99: { 82 + label: "P99", 83 + variant: "default", 84 + }, 85 + } as const satisfies Record< 86 + keyof ReturnType<typeof mapMetrics>[number], 87 + { 88 + label: string; 89 + variant: React.ComponentProps<typeof MetricCard>["variant"]; 90 + } 91 + >; 92 + 93 + export function mapUptime(status: RouterOutputs["tinybird"]["uptime"]) { 94 + return status.data 95 + .map((status) => { 96 + return { 97 + ...status, 98 + ok: status.success, 99 + interval: formatDateTime(status.interval), 100 + total: status.success + status.error + status.degraded, 101 + }; 102 + }) 103 + .reverse(); 104 + } 105 + 106 + /** 107 + * Transform Tinybird `metricsRegions` response into RegionMetric[] for UI. 108 + */ 109 + export function mapRegionMetrics( 110 + timeline: RouterOutputs["tinybird"]["metricsRegions"] | undefined, 111 + regions: Region[], 112 + percentile: (typeof PERCENTILES)[number], 113 + ): RegionMetric[] { 114 + if (!timeline) 115 + return (regions 116 + .sort((a, b) => a.localeCompare(b)) 117 + .map((region) => ({ 118 + region, 119 + p50: 0, 120 + p90: 0, 121 + p99: 0, 122 + trend: [] as { 123 + latency: number; 124 + timestamp: number; 125 + [key: string]: number; 126 + }[], 127 + })) ?? []) as RegionMetric[]; 128 + 129 + type TimelineRow = (typeof timeline.data)[number]; 130 + 131 + const map = new Map< 132 + Region, 133 + { 134 + region: Region; 135 + p50: number; 136 + p90: number; 137 + p99: number; 138 + trend: { 139 + latency: number; 140 + timestamp: number; 141 + [key: string]: number; 142 + }[]; 143 + } 144 + >(); 145 + 146 + (timeline.data as TimelineRow[]) 147 + .filter((row) => regions.includes(row.region as Region)) 148 + .sort((a, b) => a.region.localeCompare(b.region)) 149 + .forEach((row) => { 150 + const region = row.region as Region; 151 + const entry = map.get(region) ?? { 152 + region, 153 + p50: 0, 154 + p90: 0, 155 + p99: 0, 156 + trend: [], 157 + }; 158 + 159 + entry.trend.push({ 160 + latency: row[PERCENTILE_MAP[percentile]] ?? 0, 161 + timestamp: row.timestamp, 162 + [region]: row[PERCENTILE_MAP[percentile]] ?? 0, 163 + }); 164 + 165 + entry.p50 += row.p50Latency ?? 0; 166 + entry.p90 += row.p90Latency ?? 0; 167 + entry.p99 += row.p99Latency ?? 0; 168 + 169 + map.set(region, entry); 170 + }); 171 + 172 + map.forEach((entry) => { 173 + const count = entry.trend.length || 1; 174 + entry.trend.reverse(); 175 + entry.p50 = Math.round(entry.p50 / count); 176 + entry.p90 = Math.round(entry.p90 / count); 177 + entry.p99 = Math.round(entry.p99 / count); 178 + }); 179 + 180 + return Array.from(map.values()) as RegionMetric[]; 181 + } 182 + 183 + export function mapGlobalMetrics( 184 + metrics: RouterOutputs["tinybird"]["globalMetrics"], 185 + ) { 186 + return metrics.data?.map((metric) => { 187 + return { 188 + p50: metric.p50Latency, 189 + p75: metric.p75Latency, 190 + p90: metric.p90Latency, 191 + p95: metric.p95Latency, 192 + p99: metric.p99Latency, 193 + total: metric.count, 194 + monitorId: metric.monitorId, 195 + }; 196 + }); 197 + } 198 + 199 + export type MonitorListMetric = { 200 + title: string; 201 + key: "degraded" | "error" | "active" | "inactive" | "p95"; 202 + value: number | string | undefined; 203 + variant: React.ComponentProps<typeof MetricCard>["variant"]; 204 + }; 205 + 206 + export const globalCards = [ 207 + "active", 208 + "degraded", 209 + "error", 210 + "inactive", 211 + "p95", 212 + ] as const; 213 + 214 + export const metricsGlobalCards: Record< 215 + (typeof globalCards)[number], 216 + { 217 + title: string; 218 + key: (typeof globalCards)[number]; 219 + } 220 + > = { 221 + active: { 222 + title: "Normal", 223 + key: "active" as const, 224 + }, 225 + degraded: { 226 + title: "Degraded", 227 + key: "degraded" as const, 228 + }, 229 + error: { 230 + title: "Failing", 231 + key: "error" as const, 232 + }, 233 + inactive: { 234 + title: "Inactive", 235 + key: "inactive" as const, 236 + }, 237 + p95: { 238 + title: "Slowest P95", 239 + key: "p95" as const, 240 + }, 241 + }; 242 + 243 + /** 244 + * Build the metric cards data that is shown on the monitors list page. 245 + */ 246 + export function getMonitorListMetrics( 247 + monitors: RouterOutputs["monitor"]["list"] = [], 248 + data: { 249 + p95Latency: number; 250 + monitorId: string; 251 + }[] = [], 252 + ): readonly MonitorListMetric[] { 253 + const variantMap: Record< 254 + (typeof globalCards)[number], 255 + React.ComponentProps<typeof MetricCard>["variant"] 256 + > = { 257 + active: "success", 258 + degraded: "warning", 259 + error: "destructive", 260 + inactive: "default", 261 + p95: "ghost", 262 + } as const; 263 + 264 + return globalCards.map((key) => { 265 + let value: number | string | undefined; 266 + switch (key) { 267 + case "active": 268 + value = monitors.filter( 269 + (m) => m.status === "active" && m.active, 270 + ).length; 271 + break; 272 + case "degraded": 273 + value = monitors.filter( 274 + (m) => m.status === "degraded" && m.active, 275 + ).length; 276 + break; 277 + case "error": 278 + value = monitors.filter((m) => m.status === "error" && m.active).length; 279 + break; 280 + case "inactive": 281 + value = monitors.filter((m) => m.active === false).length; 282 + break; 283 + case "p95": 284 + const p95 = data.sort((a, b) => b.p95Latency - a.p95Latency)[0] 285 + ?.p95Latency; 286 + value = p95 ? formatMilliseconds(p95) : "N/A"; 287 + break; 288 + } 289 + 290 + return { 291 + title: metricsGlobalCards[key].title, 292 + key, 293 + value, 294 + variant: variantMap[key], 295 + } as const; 296 + }) as readonly MonitorListMetric[]; 297 + } 298 + 299 + export function mapLatency( 300 + latency: RouterOutputs["tinybird"]["metricsLatency"], 301 + percentile: (typeof PERCENTILES)[number], 302 + ) { 303 + return latency.data?.map((metric) => { 304 + return { 305 + timestamp: formatDateTime(new Date(metric.timestamp)), 306 + latency: metric[PERCENTILE_MAP[percentile]], 307 + }; 308 + }); 309 + } 310 + 311 + export function mapTimingPhases( 312 + timingPhases: RouterOutputs["tinybird"]["metricsTimingPhases"], 313 + percentile: (typeof PERCENTILES)[number], 314 + ) { 315 + return timingPhases.data?.map((metric) => { 316 + return { 317 + timestamp: formatDateTime(new Date(metric.timestamp)), 318 + dns: metric[`${percentile}Dns`], 319 + ttfb: metric[`${percentile}Ttfb`], 320 + transfer: metric[`${percentile}Transfer`], 321 + connect: metric[`${percentile}Connect`], 322 + tls: metric[`${percentile}Tls`], 323 + }; 324 + }); 325 + }
-78
apps/status-page/src/data/notifiers.client.ts
··· 1 - import { FormDiscord } from "@/components/forms/notifier/form-discord"; 2 - import { FormEmail } from "@/components/forms/notifier/form-email"; 3 - import { FormSlack } from "@/components/forms/notifier/form-slack"; 4 - import { FormSms } from "@/components/forms/notifier/form-sms"; 5 - import { FormWebhook } from "@/components/forms/notifier/form-webhook"; 6 - import { DiscordIcon } from "@/components/icons/discord"; 7 - import { OpsGenieIcon } from "@/components/icons/opsgenie"; 8 - import { PagerDutyIcon } from "@/components/icons/pagerduty"; 9 - import { SlackIcon } from "@/components/icons/slack"; 10 - import { Mail, MessageCircle, Pencil, Trash2, Webhook } from "lucide-react"; 11 - 12 - export const actions = [ 13 - { 14 - id: "edit", 15 - label: "Edit", 16 - icon: Pencil, 17 - variant: "default" as const, 18 - }, 19 - { 20 - id: "delete", 21 - label: "Delete", 22 - icon: Trash2, 23 - variant: "destructive" as const, 24 - }, 25 - ] as const; 26 - 27 - export type NotifierAction = (typeof actions)[number]; 28 - 29 - export const getActions = ( 30 - props: Partial<Record<NotifierAction["id"], () => Promise<void> | void>>, 31 - ): (NotifierAction & { onClick?: () => Promise<void> | void })[] => { 32 - return actions.map((action) => ({ 33 - ...action, 34 - onClick: props[action.id as keyof typeof props], 35 - })); 36 - }; 37 - 38 - // List of the notifiers 39 - 40 - export const config = { 41 - slack: { 42 - icon: SlackIcon, 43 - label: "Slack", 44 - form: FormSlack, 45 - }, 46 - discord: { 47 - icon: DiscordIcon, 48 - label: "Discord", 49 - form: FormDiscord, 50 - }, 51 - email: { 52 - icon: Mail, 53 - label: "Email", 54 - form: FormEmail, 55 - }, 56 - sms: { 57 - icon: MessageCircle, 58 - label: "SMS", 59 - form: FormSms, 60 - }, 61 - webhook: { 62 - icon: Webhook, 63 - label: "Webhook", 64 - form: FormWebhook, 65 - }, 66 - opsgenie: { 67 - icon: OpsGenieIcon, 68 - label: "OpsGenie", 69 - form: undefined, 70 - }, 71 - pagerduty: { 72 - icon: PagerDutyIcon, 73 - label: "PagerDuty", 74 - form: undefined, 75 - }, 76 - }; 77 - 78 - export type NotifierProvider = keyof typeof config;
-10
apps/status-page/src/data/notifiers.ts
··· 1 - export const notifiers = [ 2 - { 3 - id: 1, 4 - name: "Email", 5 - provider: "email", 6 - value: "max@openstatus.dev", 7 - }, 8 - ]; 9 - 10 - export type Notifier = (typeof notifiers)[number];
-16
apps/status-page/src/data/subscribers.ts
··· 1 - export const subscribers = [ 2 - { 3 - id: "1", 4 - email: "max@openstatus.dev", 5 - createdAt: "2025-05-20", 6 - validatedAt: "2025-05-20", 7 - }, 8 - { 9 - id: "2", 10 - email: "thibault@openstatus.dev", 11 - createdAt: "2025-05-20", 12 - validatedAt: "2025-05-20", 13 - }, 14 - ]; 15 - 16 - export type Subscriber = (typeof subscribers)[number];
+5
apps/status-page/src/next-auth.d.ts
··· 1 + import type { User as DefaultUserSchema } from "@openstatus/db/src/schema"; 2 + 3 + declare module "next-auth" { 4 + interface User extends DefaultUserSchema {} 5 + }
+37 -16
pnpm-lock.yaml
··· 64 64 version: 0.15.9(bufferutil@4.0.8)(utf-8-validate@6.0.5) 65 65 '@openpanel/nextjs': 66 66 specifier: 1.0.8 67 - version: 1.0.8(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 67 + version: 1.0.8(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 68 68 '@openstatus/analytics': 69 69 specifier: workspace:* 70 70 version: link:../../packages/analytics ··· 181 181 version: 1.2.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 182 182 '@sentry/nextjs': 183 183 specifier: 8.46.0 184 - version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1) 184 + version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1) 185 185 '@stripe/stripe-js': 186 186 specifier: 2.1.6 187 187 version: 2.1.6 ··· 196 196 version: 11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2) 197 197 '@trpc/next': 198 198 specifier: 11.4.4 199 - version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 199 + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 200 200 '@trpc/react-query': 201 201 specifier: 11.4.4 202 202 version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) ··· 232 232 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 233 233 nuqs: 234 234 specifier: 2.4.3 235 - version: 2.4.3(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) 235 + version: 2.4.3(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) 236 236 random-word-slugs: 237 237 specifier: 0.1.7 238 238 version: 0.1.7 ··· 486 486 487 487 apps/status-page: 488 488 dependencies: 489 + '@auth/core': 490 + specifier: 0.40.0 491 + version: 0.40.0 492 + '@auth/drizzle-adapter': 493 + specifier: 1.10.0 494 + version: 1.10.0 489 495 '@date-fns/tz': 490 496 specifier: 1.2.0 491 497 version: 1.2.0 ··· 515 521 version: 0.15.9(bufferutil@4.0.8)(utf-8-validate@6.0.5) 516 522 '@openpanel/nextjs': 517 523 specifier: 1.0.8 518 - version: 1.0.8(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 524 + version: 1.0.8(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 519 525 '@openstatus/analytics': 520 526 specifier: workspace:* 521 527 version: link:../../packages/analytics ··· 632 638 version: 1.2.7(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 633 639 '@sentry/nextjs': 634 640 specifier: 8.46.0 635 - version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1) 641 + version: 8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1) 642 + '@stripe/stripe-js': 643 + specifier: 2.1.6 644 + version: 2.1.6 636 645 '@tanstack/react-query': 637 646 specifier: 5.81.5 638 647 version: 5.81.5(react@19.1.1) ··· 644 653 version: 11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2) 645 654 '@trpc/next': 646 655 specifier: 11.4.4 647 - version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 656 + version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 648 657 '@trpc/react-query': 649 658 specifier: 11.4.4 650 659 version: 11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) ··· 672 681 next: 673 682 specifier: 15.4.7 674 683 version: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 684 + next-auth: 685 + specifier: 5.0.0-beta.29 686 + version: 5.0.0-beta.29(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) 675 687 next-themes: 676 688 specifier: 0.4.6 677 689 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 678 690 nuqs: 679 691 specifier: 2.4.3 680 - version: 2.4.3(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) 692 + version: 2.4.3(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) 693 + random-word-slugs: 694 + specifier: 0.1.7 695 + version: 0.1.7 681 696 react: 682 697 specifier: 19.1.1 683 698 version: 19.1.1 ··· 708 723 sonner: 709 724 specifier: 2.0.5 710 725 version: 2.0.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 726 + stripe: 727 + specifier: 13.8.0 728 + version: 13.8.0 711 729 superjson: 712 730 specifier: 2.2.2 713 731 version: 2.2.2 ··· 724 742 '@tailwindcss/postcss': 725 743 specifier: 4.1.11 726 744 version: 4.1.11 745 + '@types/dom-speech-recognition': 746 + specifier: 0.0.6 747 + version: 0.0.6 727 748 '@types/node': 728 749 specifier: 24.0.8 729 750 version: 24.0.8 ··· 765 786 version: 0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5) 766 787 '@openpanel/nextjs': 767 788 specifier: ^1.0.8 768 - version: 1.0.8(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 789 + version: 1.0.8(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 769 790 '@openstatus/analytics': 770 791 specifier: workspace:* 771 792 version: link:../../packages/analytics ··· 14663 14684 '@openpanel/web': 1.0.1 14664 14685 astro: 5.12.2(@types/node@24.0.8)(encoding@0.1.13)(jiti@2.4.2)(lightningcss@1.30.1)(rollup@4.45.1)(terser@5.43.1)(typescript@5.7.2)(yaml@2.6.1) 14665 14686 14666 - '@openpanel/nextjs@1.0.8(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': 14687 + '@openpanel/nextjs@1.0.8(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': 14667 14688 dependencies: 14668 14689 '@openpanel/web': 1.0.1 14669 14690 next: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) ··· 16683 16704 '@sentry/types': 8.9.2 16684 16705 '@sentry/utils': 8.9.2 16685 16706 16686 - '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1(esbuild@0.21.5))': 16707 + '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1)': 16687 16708 dependencies: 16688 16709 '@opentelemetry/api': 1.9.0 16689 16710 '@opentelemetry/semantic-conventions': 1.28.0 ··· 16694 16715 '@sentry/opentelemetry': 8.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0) 16695 16716 '@sentry/react': 8.46.0(react@19.1.1) 16696 16717 '@sentry/vercel-edge': 8.46.0 16697 - '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(esbuild@0.21.5)) 16718 + '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1) 16698 16719 chalk: 3.0.0 16699 16720 next: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 16700 16721 resolve: 1.22.8 ··· 16709 16730 - supports-color 16710 16731 - webpack 16711 16732 16712 - '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1)': 16733 + '@sentry/nextjs@8.46.0(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)(webpack@5.97.1(esbuild@0.21.5))': 16713 16734 dependencies: 16714 16735 '@opentelemetry/api': 1.9.0 16715 16736 '@opentelemetry/semantic-conventions': 1.28.0 ··· 16720 16741 '@sentry/opentelemetry': 8.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0) 16721 16742 '@sentry/react': 8.46.0(react@19.1.1) 16722 16743 '@sentry/vercel-edge': 8.46.0 16723 - '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1) 16744 + '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(esbuild@0.21.5)) 16724 16745 chalk: 3.0.0 16725 16746 next: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 16726 16747 resolve: 1.22.8 ··· 17432 17453 '@tanstack/react-query': 5.80.7(react@19.1.1) 17433 17454 '@trpc/react-query': 11.4.4(@tanstack/react-query@5.80.7(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2) 17434 17455 17435 - '@trpc/next@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2)': 17456 + '@trpc/next@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/react-query@11.4.4(@tanstack/react-query@5.81.5(react@19.1.1))(@trpc/client@11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2))(@trpc/server@11.4.4(typescript@5.7.2))(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.7.2)': 17436 17457 dependencies: 17437 17458 '@trpc/client': 11.4.4(@trpc/server@11.4.4(typescript@5.7.2))(typescript@5.7.2) 17438 17459 '@trpc/server': 11.4.4(typescript@5.7.2) ··· 21779 21800 optionalDependencies: 21780 21801 next: 15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) 21781 21802 21782 - nuqs@2.4.3(next@15.4.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): 21803 + nuqs@2.4.3(next@15.4.7(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1): 21783 21804 dependencies: 21784 21805 mitt: 3.0.1 21785 21806 react: 19.1.1