Openstatus www.openstatus.dev

feat: grouped monitors (#1524)

* feat: grouped monitors

* chore: tooltip

* chore: db migration

* wip: status page

* fix: input bg

* chore: add changelog

authored by

Maximilian Kaske and committed by
GitHub
1d89615b 92961201

+3661 -145
+569 -93
apps/dashboard/src/components/forms/status-page/form-monitors.tsx
··· 15 15 FormCardSeparator, 16 16 FormCardTitle, 17 17 } from "@/components/forms/form-card"; 18 + import { 19 + AlertDialog, 20 + AlertDialogAction, 21 + AlertDialogCancel, 22 + AlertDialogContent, 23 + AlertDialogDescription, 24 + AlertDialogFooter, 25 + AlertDialogHeader, 26 + AlertDialogTitle, 27 + AlertDialogTrigger, 28 + } from "@/components/ui/alert-dialog"; 18 29 import { Button } from "@/components/ui/button"; 19 30 import { 20 31 Command, ··· 33 44 FormLabel, 34 45 FormMessage, 35 46 } from "@/components/ui/form"; 47 + import { Input } from "@/components/ui/input"; 36 48 import { PopoverContent } from "@/components/ui/popover"; 37 49 import { Popover, PopoverTrigger } from "@/components/ui/popover"; 38 50 import { ··· 42 54 SortableItemHandle, 43 55 SortableOverlay, 44 56 } from "@/components/ui/sortable"; 57 + import { 58 + Tooltip, 59 + TooltipContent, 60 + TooltipProvider, 61 + TooltipTrigger, 62 + } from "@/components/ui/tooltip"; 45 63 import { cn } from "@/lib/utils"; 46 64 import type { UniqueIdentifier } from "@dnd-kit/core"; 47 65 import { zodResolver } from "@hookform/resolvers/zod"; 48 66 import { isTRPCClientError } from "@trpc/client"; 49 - import { Check, ChevronsUpDown, GripVertical } from "lucide-react"; 67 + import { 68 + Check, 69 + ChevronsUpDown, 70 + GripVertical, 71 + Plus, 72 + Trash2, 73 + } from "lucide-react"; 50 74 import { useCallback, useEffect, useState, useTransition } from "react"; 51 75 import { type UseFormReturn, useForm } from "react-hook-form"; 52 76 import { toast } from "sonner"; ··· 59 83 active: boolean | null; 60 84 }; 61 85 86 + type MonitorGroup = { 87 + id: number; 88 + name: string; 89 + monitors: Monitor[]; 90 + }; 91 + 92 + const monitorSchema = z.object({ 93 + id: z.number(), 94 + order: z.number(), 95 + active: z.boolean().nullable(), 96 + }); 97 + 62 98 const schema = z.object({ 63 - monitors: z.array( 99 + monitors: z.array(monitorSchema), 100 + groups: z.array( 64 101 z.object({ 65 102 id: z.number(), 66 103 order: z.number(), 67 - active: z.boolean().nullable(), 104 + name: z.string(), 105 + monitors: z 106 + .array(monitorSchema) 107 + .min(1, { message: "At least one monitor is required" }), 68 108 }), 69 109 ), 70 110 }); ··· 82 122 const bOrder = orderMap.get(b.id) ?? 0; 83 123 return aOrder - bOrder; 84 124 }); 125 + }; 126 + 127 + const getSortedItems = ( 128 + monitors: Monitor[], 129 + monitorData: { id: number; order: number }[], 130 + groups: Array<{ 131 + id: number; 132 + order: number; 133 + name: string; 134 + monitors: Array<{ id: number; order: number; active: boolean | null }>; 135 + }>, 136 + ): (Monitor | MonitorGroup)[] => { 137 + // Create map of monitor orders 138 + const monitorOrderMap = new Map(monitorData.map((m) => [m.id, m.order])); 139 + 140 + // Create array of monitors with their orders 141 + const monitorsWithOrder = monitors 142 + .filter((monitor) => monitorOrderMap.has(monitor.id)) 143 + .map((monitor) => ({ 144 + item: monitor, 145 + order: monitorOrderMap.get(monitor.id) ?? 0, 146 + })); 147 + 148 + // Create array of groups with their orders 149 + const groupsWithOrder = groups.map((group) => ({ 150 + item: { 151 + id: group.id, 152 + name: group.name, 153 + monitors: getSortedMonitors(monitors, group.monitors), 154 + } as MonitorGroup, 155 + order: group.order, 156 + })); 157 + 158 + // Combine and sort by order 159 + return [...monitorsWithOrder, ...groupsWithOrder] 160 + .sort((a, b) => a.order - b.order) 161 + .map((entry) => entry.item); 85 162 }; 86 163 87 164 type FormValues = z.infer<typeof schema>; ··· 90 167 defaultValues, 91 168 onSubmit, 92 169 monitors, 170 + legacy, 93 171 ...props 94 172 }: Omit<React.ComponentProps<"form">, "onSubmit"> & { 95 173 defaultValues?: FormValues; 96 174 monitors: Monitor[]; 175 + /** 176 + * Whether the status page is legacy or new 177 + */ 178 + legacy: boolean; 97 179 onSubmit: (values: FormValues) => Promise<void>; 98 180 }) { 99 181 const form = useForm<FormValues>({ ··· 102 184 }); 103 185 const [isPending, startTransition] = useTransition(); 104 186 const watchMonitors = form.watch("monitors"); 105 - const [data, setData] = useState<Monitor[]>( 106 - getSortedMonitors(monitors, defaultValues?.monitors ?? []), 187 + const watchGroups = form.watch("groups"); 188 + const [data, setData] = useState<(Monitor | MonitorGroup)[]>( 189 + getSortedItems( 190 + monitors, 191 + defaultValues?.monitors ?? [], 192 + defaultValues?.groups ?? [], 193 + ), 107 194 ); 108 195 109 - // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 196 + // Get all monitor IDs that are already used in groups 197 + const monitorsInGroups = new Set( 198 + (watchGroups ?? []).flatMap((g) => g.monitors.map((m) => m.id)), 199 + ); 200 + 110 201 useEffect(() => { 111 - if (watchMonitors.length !== data.length) { 112 - setData(getSortedMonitors(monitors, watchMonitors)); 113 - } 114 - // eslint-disable-next-line react-hooks/exhaustive-deps 115 - }, [watchMonitors]); 202 + const sortedItems = getSortedItems( 203 + monitors, 204 + watchMonitors, 205 + watchGroups ?? [], 206 + ); 207 + setData(sortedItems); 208 + }, [watchMonitors, watchGroups, monitors]); 116 209 117 210 const onValueChange = useCallback( 118 - (newMonitors: Monitor[]) => { 119 - setData(newMonitors); 211 + (newItems: (Monitor | MonitorGroup)[]) => { 212 + setData(newItems); 213 + 214 + // Update monitors with their position in the overall list 215 + const monitors = newItems 216 + .map((item, index) => ({ item, index })) 217 + .filter( 218 + (entry): entry is { item: Monitor; index: number } => 219 + "url" in entry.item, 220 + ) 221 + .map(({ item, index }) => ({ 222 + id: item.id, 223 + order: index, 224 + active: item.active, 225 + })); 226 + form.setValue("monitors", monitors); 227 + 228 + // Update groups with their position in the overall list 229 + const existingGroups = form.getValues("groups") ?? []; 230 + const groups = newItems 231 + .map((item, index) => ({ item, index })) 232 + .filter( 233 + (entry): entry is { item: MonitorGroup; index: number } => 234 + "monitors" in entry.item && !("url" in entry.item), 235 + ) 236 + .map(({ item, index }) => { 237 + const existingGroup = existingGroups.find((g) => g.id === item.id); 238 + return existingGroup 239 + ? { 240 + ...existingGroup, 241 + order: index, 242 + } 243 + : { 244 + id: item.id, 245 + order: index, 246 + name: item.name, 247 + monitors: [], 248 + }; 249 + }); 250 + form.setValue("groups", groups); 251 + }, 252 + [form], 253 + ); 254 + 255 + const getItemValue = useCallback( 256 + (item: Monitor | MonitorGroup) => item.id, 257 + [], 258 + ); 259 + 260 + const handleAddGroup = useCallback(() => { 261 + const newGroupId = Date.now(); 262 + const existingGroups = form.getValues("groups") ?? []; 263 + const existingMonitors = form.getValues("monitors") ?? []; 264 + const order = existingGroups.length + existingMonitors.length; 265 + const newGroups = [ 266 + ...existingGroups, 267 + { id: newGroupId, order, name: "", monitors: [] }, 268 + ]; 269 + form.setValue("groups", newGroups); 270 + setData((prev) => [ 271 + ...prev, 272 + { id: newGroupId, order, name: "", monitors: [] }, 273 + ]); 274 + }, [form]); 275 + 276 + const handleDeleteGroup = useCallback( 277 + (groupId: number) => { 278 + const existingGroups = form.getValues("groups") ?? []; 120 279 form.setValue( 121 - "monitors", 122 - newMonitors.map((m, index) => ({ 123 - id: m.id, 124 - order: index, 125 - active: m.active, 126 - })), 280 + "groups", 281 + existingGroups.filter((g) => g.id !== groupId), 127 282 ); 283 + setData((prev) => prev.filter((item) => item.id !== groupId)); 128 284 }, 129 285 [form], 130 286 ); 131 287 132 - const getItemValue = useCallback((item: Monitor) => item.id, []); 133 - 134 - // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation> 135 288 const renderOverlay = useCallback( 136 289 ({ value }: { value: UniqueIdentifier }) => { 137 - const monitor = data.find((monitor) => monitor.id === value); 290 + const monitor = data.find((item) => item.id === value); 138 291 if (!monitor) return null; 139 292 140 - return <MonitorRow monitor={monitor} form={form} />; 293 + if ("url" in monitor) { 294 + return ( 295 + <MonitorRow 296 + monitor={monitor} 297 + form={form} 298 + className="border-transparent border-x px-2" 299 + /> 300 + ); 301 + } 302 + 303 + const groups = form.getValues("groups") ?? []; 304 + const groupIndex = groups.findIndex((g) => g.id === monitor.id); 305 + return ( 306 + <MonitorGroup 307 + group={monitor} 308 + groupIndex={groupIndex} 309 + onDeleteGroup={handleDeleteGroup} 310 + form={form} 311 + monitors={monitors} 312 + /> 313 + ); 141 314 }, 142 - // eslint-disable-next-line react-hooks/exhaustive-deps 143 - [data], 315 + [data, handleDeleteGroup, form, monitors], 144 316 ); 145 317 146 318 function submitAction(values: FormValues) { 147 319 if (isPending) return; 320 + 321 + console.log("submitAction", values); 148 322 149 323 startTransition(async () => { 150 324 try { ··· 176 350 Connect your monitors to your status page. 177 351 </FormCardDescription> 178 352 </FormCardHeader> 179 - <FormCardContent> 353 + <FormCardContent className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> 180 354 <FormField 181 355 control={form.control} 182 356 name="monitors" ··· 190 364 variant="outline" 191 365 role="combobox" 192 366 className={cn( 193 - "w-[200px] justify-between", 367 + "w-full justify-between", 194 368 !field.value && "text-muted-foreground", 195 369 )} 196 370 > ··· 201 375 </Button> 202 376 </FormControl> 203 377 </PopoverTrigger> 204 - <PopoverContent className="w-[200px] p-0"> 378 + <PopoverContent className="p-0"> 205 379 <Command> 206 380 <CommandInput 207 381 placeholder="Search monitors..." ··· 210 384 <CommandList> 211 385 <CommandEmpty>No monitors found.</CommandEmpty> 212 386 <CommandGroup> 213 - {monitors.map((monitor) => ( 214 - <CommandItem 215 - value={monitor.name} 216 - key={monitor.id} 217 - onSelect={() => { 218 - if ( 219 - field.value.some((m) => m.id === monitor.id) 220 - ) { 221 - form.setValue( 222 - "monitors", 223 - field.value.filter( 224 - (m) => m.id !== monitor.id, 225 - ), 226 - ); 227 - } else { 228 - form.setValue("monitors", [ 229 - ...field.value, 230 - { 231 - id: monitor.id, 232 - order: watchMonitors.length, 233 - active: monitor.active, 234 - }, 235 - ]); 236 - } 237 - }} 238 - > 239 - {monitor.name} 240 - <Check 241 - className={cn( 242 - "ml-auto", 243 - field.value.some((m) => m.id === monitor.id) 244 - ? "opacity-100" 245 - : "opacity-0", 246 - )} 247 - /> 248 - </CommandItem> 249 - ))} 387 + {monitors.map((monitor) => { 388 + const isInGroup = monitorsInGroups.has( 389 + monitor.id, 390 + ); 391 + const isSelected = field.value.some( 392 + (m) => m.id === monitor.id, 393 + ); 394 + return ( 395 + <CommandItem 396 + value={monitor.name} 397 + key={monitor.id} 398 + disabled={isInGroup} 399 + onSelect={() => { 400 + if (isSelected) { 401 + form.setValue( 402 + "monitors", 403 + field.value.filter( 404 + (m) => m.id !== monitor.id, 405 + ), 406 + ); 407 + } else { 408 + form.setValue("monitors", [ 409 + ...field.value, 410 + { 411 + id: monitor.id, 412 + order: watchMonitors.length, 413 + active: monitor.active, 414 + }, 415 + ]); 416 + } 417 + }} 418 + > 419 + {monitor.name} 420 + <Check 421 + className={cn( 422 + "ml-auto", 423 + isSelected ? "opacity-100" : "opacity-0", 424 + )} 425 + /> 426 + </CommandItem> 427 + ); 428 + })} 250 429 </CommandGroup> 251 430 </CommandList> 252 431 </Command> 253 432 </PopoverContent> 254 433 </Popover> 255 - <FormDescription> 256 - Select the monitors you want to display on your status page. 257 - </FormDescription> 434 + <FormDescription>Choose monitors to display.</FormDescription> 258 435 <FormMessage /> 259 436 </FormItem> 260 437 )} 261 438 /> 439 + {legacy ? ( 440 + <TooltipProvider> 441 + <Tooltip> 442 + <TooltipTrigger asChild> 443 + <span className="w-full"> 444 + <Button 445 + variant="outline" 446 + type="button" 447 + className="w-full" 448 + disabled={legacy} 449 + > 450 + <Plus /> 451 + Add Group 452 + </Button> 453 + </span> 454 + </TooltipTrigger> 455 + <TooltipContent> 456 + <p> 457 + Enable the new redesign to add groups to your status page. 458 + </p> 459 + </TooltipContent> 460 + </Tooltip> 461 + </TooltipProvider> 462 + ) : ( 463 + <Button 464 + variant="outline" 465 + type="button" 466 + className="w-full" 467 + onClick={handleAddGroup} 468 + > 469 + <Plus /> 470 + Add Group 471 + </Button> 472 + )} 262 473 </FormCardContent> 263 474 <FormCardSeparator /> 264 475 <FormCardContent> 265 - {data.length ? ( 266 - <Sortable 267 - value={data} 268 - onValueChange={onValueChange} 269 - getItemValue={getItemValue} 270 - orientation="vertical" 271 - > 476 + <Sortable 477 + value={data} 478 + onValueChange={onValueChange} 479 + getItemValue={getItemValue} 480 + orientation="vertical" 481 + > 482 + {data.length ? ( 272 483 <SortableContent className="grid gap-2"> 273 - {data.map((monitor) => ( 274 - <MonitorRow 275 - key={monitor.id} 276 - monitor={monitor} 277 - form={form} 278 - /> 279 - ))} 484 + {data.map((item) => { 485 + if ("url" in item) { 486 + return ( 487 + <MonitorRow 488 + key={`${item.id}-monitor`} 489 + className="border-transparent border-x px-2" 490 + monitor={item} 491 + form={form} 492 + /> 493 + ); 494 + } 495 + const groups = form.getValues("groups") ?? []; 496 + const groupIndex = groups.findIndex( 497 + (g) => g.id === item.id, 498 + ); 499 + return ( 500 + <MonitorGroup 501 + key={`${item.id}-group`} 502 + group={item} 503 + groupIndex={groupIndex} 504 + onDeleteGroup={handleDeleteGroup} 505 + form={form} 506 + monitors={monitors} 507 + /> 508 + ); 509 + })} 280 510 <SortableOverlay>{renderOverlay}</SortableOverlay> 281 511 </SortableContent> 282 - </Sortable> 283 - ) : ( 284 - <EmptyStateContainer> 285 - <EmptyStateTitle>No monitors selected</EmptyStateTitle> 286 - </EmptyStateContainer> 287 - )} 512 + ) : ( 513 + <EmptyStateContainer> 514 + <EmptyStateTitle>No monitors selected</EmptyStateTitle> 515 + </EmptyStateContainer> 516 + )} 517 + </Sortable> 288 518 </FormCardContent> 289 519 <FormCardFooter> 290 520 <FormCardFooterInfo> ··· 306 536 form: UseFormReturn<FormValues>; 307 537 } 308 538 309 - function MonitorRow({ monitor, ...props }: MonitorRowProps) { 539 + function MonitorRow({ monitor, className, ...props }: MonitorRowProps) { 310 540 return ( 311 - <SortableItem value={monitor.id} asChild {...props}> 312 - <div className="grid grid-cols-3 gap-2"> 541 + <SortableItem 542 + value={monitor.id} 543 + asChild 544 + className={cn("rounded-md", className)} 545 + {...props} 546 + > 547 + <div className="grid h-9 grid-cols-3 gap-2"> 313 548 <div className="flex flex-row items-center gap-4 self-center"> 314 549 <SortableItemHandle> 315 550 <GripVertical ··· 323 558 <div className="self-center truncate text-muted-foreground text-sm"> 324 559 {monitor.url} 325 560 </div> 326 - <div className="truncate text-muted-foreground text-sm"> 561 + <div className="self-center truncate text-muted-foreground text-sm"> 327 562 {monitor.active ? "Active" : "Inactive"} 328 563 </div> 329 564 </div> 330 565 </SortableItem> 331 566 ); 332 567 } 568 + 569 + interface MonitorGroupProps 570 + extends Omit<React.ComponentPropsWithoutRef<typeof SortableItem>, "value"> { 571 + group: MonitorGroup; 572 + groupIndex: number; 573 + onDeleteGroup: (groupId: number) => void; 574 + form: UseFormReturn<FormValues>; 575 + monitors: Monitor[]; 576 + } 577 + 578 + function MonitorGroup({ 579 + group, 580 + groupIndex, 581 + onDeleteGroup, 582 + form, 583 + monitors, 584 + }: MonitorGroupProps) { 585 + const watchGroup = form.watch(`groups.${groupIndex}`); 586 + const watchMonitors = form.watch("monitors"); 587 + const watchGroups = form.watch("groups"); 588 + const [data, setData] = useState<Monitor[]>(group.monitors); 589 + 590 + // Calculate taken monitors (in main list or other groups) 591 + const takenMonitorIds = new Set([ 592 + ...watchMonitors.map((m) => m.id), 593 + ...watchGroups 594 + .filter((g) => g.id !== group.id) 595 + .flatMap((g) => g.monitors.map((m) => m.id)), 596 + ]); 597 + 598 + const onValueChange = useCallback( 599 + (newMonitors: Monitor[]) => { 600 + setData(newMonitors); 601 + // Update the form with the new monitor order 602 + form.setValue( 603 + `groups.${groupIndex}.monitors`, 604 + newMonitors.map((m, index) => ({ 605 + id: m.id, 606 + order: index, 607 + active: m.active, 608 + })), 609 + ); 610 + }, 611 + [form, groupIndex], 612 + ); 613 + 614 + useEffect(() => { 615 + setData(getSortedMonitors(monitors, watchGroup.monitors)); 616 + }, [watchGroup.monitors, monitors]); 617 + 618 + const getItemValue = useCallback((item: Monitor) => item.id, []); 619 + 620 + const renderOverlay = useCallback( 621 + ({ value }: { value: UniqueIdentifier }) => { 622 + const monitor = data.find((item) => item.id === value); 623 + if (!monitor) return null; 624 + 625 + return <MonitorRow monitor={monitor} form={form} />; 626 + }, 627 + [data, form], 628 + ); 629 + 630 + return ( 631 + <SortableItem value={group.id} className="rounded-md border bg-muted"> 632 + <div className="grid grid-cols-3 gap-2 px-2 pt-2"> 633 + <div className="flex flex-row items-center gap-1 self-center"> 634 + <SortableItemHandle> 635 + <GripVertical 636 + size={16} 637 + aria-hidden="true" 638 + className="text-muted-foreground" 639 + /> 640 + </SortableItemHandle> 641 + <FormField 642 + control={form.control} 643 + name={`groups.${groupIndex}.name` as const} 644 + render={({ field }) => ( 645 + <FormItem className="w-full"> 646 + <FormLabel className="sr-only">Group name</FormLabel> 647 + <FormControl> 648 + <Input 649 + placeholder="Group Name" 650 + className="w-full bg-background" 651 + {...field} 652 + /> 653 + </FormControl> 654 + <FormMessage /> 655 + </FormItem> 656 + )} 657 + /> 658 + </div> 659 + <FormField 660 + control={form.control} 661 + name={`groups.${groupIndex}.monitors` as const} 662 + render={({ field }) => ( 663 + <FormItem className="flex w-full flex-col"> 664 + <FormLabel className="sr-only">Monitors</FormLabel> 665 + <Popover> 666 + <PopoverTrigger asChild> 667 + <FormControl> 668 + <Button 669 + variant="outline" 670 + role="combobox" 671 + className={cn( 672 + "w-full justify-between", 673 + !field.value && "text-muted-foreground", 674 + )} 675 + > 676 + {Array.isArray(field.value) && field.value.length > 0 677 + ? `${field.value.length} monitors selected` 678 + : "Select monitors"} 679 + <ChevronsUpDown className="opacity-50" /> 680 + </Button> 681 + </FormControl> 682 + </PopoverTrigger> 683 + <PopoverContent className="p-0"> 684 + <Command> 685 + <CommandInput 686 + placeholder="Search monitors..." 687 + className="h-9" 688 + /> 689 + <CommandList> 690 + <CommandEmpty>No monitors found.</CommandEmpty> 691 + <CommandGroup> 692 + {monitors.map((monitor) => { 693 + const current = field.value ?? []; 694 + const isSelected = current.some( 695 + (m) => m.id === monitor.id, 696 + ); 697 + const isTaken = takenMonitorIds.has(monitor.id); 698 + return ( 699 + <CommandItem 700 + value={monitor.name} 701 + key={monitor.id} 702 + disabled={isTaken} 703 + onSelect={() => { 704 + if (isSelected) { 705 + form.setValue( 706 + `groups.${groupIndex}.monitors`, 707 + current.filter((m) => m.id !== monitor.id), 708 + ); 709 + } else { 710 + form.setValue( 711 + `groups.${groupIndex}.monitors`, 712 + [ 713 + ...current, 714 + { 715 + id: monitor.id, 716 + order: 0, 717 + active: monitor.active, 718 + }, 719 + ], 720 + ); 721 + } 722 + }} 723 + > 724 + {monitor.name} 725 + <Check 726 + className={cn( 727 + "ml-auto", 728 + isSelected ? "opacity-100" : "opacity-0", 729 + )} 730 + /> 731 + </CommandItem> 732 + ); 733 + })} 734 + </CommandGroup> 735 + </CommandList> 736 + </Command> 737 + </PopoverContent> 738 + </Popover> 739 + <FormMessage /> 740 + </FormItem> 741 + )} 742 + /> 743 + <div className="flex justify-end"> 744 + <AlertDialog> 745 + <AlertDialogTrigger asChild> 746 + <Button 747 + type="button" 748 + variant="ghost" 749 + size="icon" 750 + className="text-destructive hover:bg-destructive/10 hover:text-destructive dark:hover:bg-destructive/20 [&_svg]:size-4 [&_svg]:text-destructive" 751 + // NOTE: delete directly if no monitors are in the group 752 + {...(data.length === 0 753 + ? { onClick: () => onDeleteGroup(group.id) } 754 + : {})} 755 + > 756 + <Trash2 /> 757 + </Button> 758 + </AlertDialogTrigger> 759 + <AlertDialogContent> 760 + <AlertDialogHeader> 761 + <AlertDialogTitle>Are you sure?</AlertDialogTitle> 762 + <AlertDialogDescription> 763 + You are about to delete this group and all its monitors. 764 + </AlertDialogDescription> 765 + </AlertDialogHeader> 766 + <AlertDialogFooter> 767 + <AlertDialogCancel>Cancel</AlertDialogCancel> 768 + <AlertDialogAction 769 + className="bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40" 770 + onClick={() => onDeleteGroup(group.id)} 771 + > 772 + Delete 773 + </AlertDialogAction> 774 + </AlertDialogFooter> 775 + </AlertDialogContent> 776 + </AlertDialog> 777 + </div> 778 + </div> 779 + <div className="mt-2 border-t px-2 pt-2 pb-2"> 780 + <Sortable 781 + value={data} 782 + onValueChange={onValueChange} 783 + getItemValue={getItemValue} 784 + orientation="vertical" 785 + > 786 + {data.length ? ( 787 + <SortableContent className="grid gap-2"> 788 + {data.map((item) => { 789 + return ( 790 + <MonitorRow 791 + key={`${item.id}-monitor`} 792 + monitor={item} 793 + form={form} 794 + /> 795 + ); 796 + })} 797 + <SortableOverlay>{renderOverlay}</SortableOverlay> 798 + </SortableContent> 799 + ) : ( 800 + <EmptyStateContainer> 801 + <EmptyStateTitle>No monitors selected</EmptyStateTitle> 802 + </EmptyStateContainer> 803 + )} 804 + </Sortable> 805 + </div> 806 + </SortableItem> 807 + ); 808 + }
+27 -5
apps/dashboard/src/components/forms/status-page/update.tsx
··· 116 116 <FormMonitors 117 117 monitors={monitors ?? []} 118 118 defaultValues={{ 119 - monitors: statusPage.monitors.map((monitor) => ({ 120 - id: monitor.id, 121 - order: monitor.order, 122 - active: monitor.active ?? null, 123 - })), 119 + monitors: statusPage.monitors 120 + .filter((m) => !m.groupId) 121 + .map((monitor) => ({ 122 + id: monitor.id, 123 + order: monitor.order, 124 + active: monitor.active ?? null, 125 + })), 126 + groups: statusPage.monitorGroups.map((group) => { 127 + const order = 128 + statusPage.monitors.find((m) => m.groupId === group.id) 129 + ?.groupOrder ?? 0; 130 + console.log(group); 131 + return { 132 + id: -1 * group.id, // negative id to avoid conflicts with monitors 133 + order, 134 + name: group.name, 135 + monitors: statusPage.monitors 136 + .filter((m) => m.groupId === group.id) 137 + .map((monitor) => ({ 138 + id: monitor.id, 139 + order: monitor.order, 140 + active: monitor.active ?? null, 141 + })), 142 + }; 143 + }), 124 144 }} 145 + legacy={statusPage.legacyPage} 125 146 onSubmit={async (values) => { 126 147 await updateMonitorsMutation.mutateAsync({ 127 148 id: Number.parseInt(id), 128 149 monitors: values.monitors, 150 + groups: values.groups, 129 151 }); 130 152 }} 131 153 />
+41 -13
apps/status-page/src/app/(status-page)/[domain]/(public)/page.tsx
··· 23 23 } from "@/components/status-page/status-events"; 24 24 import { StatusFeed } from "@/components/status-page/status-feed"; 25 25 import { StatusMonitor } from "@/components/status-page/status-monitor"; 26 + import { StatusTrackerGroup } from "@/components/status-page/status-tracker-group"; 26 27 import { Separator } from "@/components/ui/separator"; 27 28 import { useTRPC } from "@/lib/trpc/client"; 28 29 import { cn } from "@/lib/utils"; ··· 178 179 <StatusBanner status={page.status} /> 179 180 )} 180 181 {/* NOTE: check what gap feels right */} 181 - {page.monitors.length > 0 ? ( 182 + {page.trackers.length > 0 ? ( 182 183 <StatusContent className="gap-5"> 183 - {page.monitors.map((monitor) => { 184 - const { data, uptime } = 185 - uptimeData?.find((m) => m.id === monitor.id) ?? {}; 184 + {page.trackers.map((tracker) => { 185 + if (tracker.type === "monitor") { 186 + const monitor = tracker.monitor; 187 + const { data, uptime } = 188 + uptimeData?.find((m) => m.id === monitor.id) ?? {}; 189 + return ( 190 + <StatusMonitor 191 + key={`monitor-${monitor.id}`} 192 + status={monitor.status} 193 + data={data} 194 + monitor={monitor} 195 + uptime={uptime} 196 + showUptime={showUptime} 197 + isLoading={isLoading} 198 + /> 199 + ); 200 + } 201 + 186 202 return ( 187 - <StatusMonitor 188 - key={monitor.id} 189 - status={monitor.status} 190 - data={data} 191 - monitor={monitor} 192 - uptime={uptime} 193 - showUptime={showUptime} 194 - isLoading={isLoading} 195 - /> 203 + <StatusTrackerGroup 204 + key={`group-${tracker.groupId}`} 205 + title={tracker.groupName} 206 + status={tracker.status} 207 + > 208 + {tracker.monitors.map((monitor) => { 209 + const { data, uptime } = 210 + uptimeData?.find((m) => m.id === monitor.id) ?? {}; 211 + return ( 212 + <StatusMonitor 213 + key={`monitor-${monitor.id}`} 214 + status={monitor.status} 215 + data={data} 216 + monitor={monitor} 217 + uptime={uptime} 218 + showUptime={showUptime} 219 + isLoading={isLoading} 220 + /> 221 + ); 222 + })} 223 + </StatusTrackerGroup> 196 224 ); 197 225 })} 198 226 </StatusContent>
+25 -14
apps/status-page/src/components/nav/header.tsx
··· 92 92 {/* NOTE: same width as the `StatusUpdates` button */} 93 93 <div className="flex w-[150px] shrink-0"> 94 94 <div className="flex items-center justify-center"> 95 - <Link 96 - href={page?.homepageUrl || "/"} 97 - target={page?.homepageUrl ? "_blank" : undefined} 98 - rel={page?.homepageUrl ? "noreferrer" : undefined} 99 - className="rounded-full" 100 - > 101 - {page?.icon ? ( 102 - <img 103 - src={page.icon} 104 - alt={`${page.title} status page`} 105 - className="size-8 rounded-full border" 106 - /> 107 - ) : null} 108 - </Link> 95 + <Button variant="outline" size="icon" className="size-8" asChild> 96 + <Link 97 + href={page?.homepageUrl || "/"} 98 + target={page?.homepageUrl ? "_blank" : undefined} 99 + rel={page?.homepageUrl ? "noreferrer" : undefined} 100 + > 101 + {page?.icon ? ( 102 + <img 103 + src={page.icon} 104 + alt={`${page.title} status page`} 105 + className="size-8" 106 + /> 107 + ) : ( 108 + <div className="flex size-8 items-center justify-center font-mono"> 109 + {/* NOTE: show the first two letters of the title and if its multiple words, show the first letter of the first two words */} 110 + {page?.title 111 + ?.split(" ") 112 + .map((word) => word.charAt(0)) 113 + .slice(0, 2) 114 + .join("") 115 + .toUpperCase()} 116 + </div> 117 + )} 118 + </Link> 119 + </Button> 109 120 </div> 110 121 </div> 111 122 <NavDesktop className="hidden md:flex" />
+5 -5
apps/status-page/src/components/status-page/status-tracker-group.tsx
··· 10 10 export function StatusTrackerGroup({ 11 11 children, 12 12 title, 13 - variant, 13 + status, 14 14 className, 15 15 ...props 16 16 }: React.ComponentProps<typeof CollapsibleTrigger> & { 17 17 title: string; 18 - variant?: VariantType; 18 + status?: VariantType; 19 19 }) { 20 20 return ( 21 21 <Collapsible 22 22 className={cn( 23 23 "-mx-3", 24 - "rounded-lg border border-transparent hover:border-border/50 hover:bg-muted/50 data-[state=open]:border-border/50 data-[state=open]:bg-muted/50", 24 + "rounded-lg border border-transparent bg-muted/50 hover:border-border/50 data-[state=open]:border-border/50 data-[state=open]:bg-muted/50", 25 25 className, 26 26 )} 27 27 > 28 28 <CollapsibleTrigger 29 29 className={cn( 30 - "group/monitor flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 font-medium", 30 + "group/monitor flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 font-medium font-mono", 31 31 "cursor-pointer", 32 32 className, 33 33 )} 34 - data-variant={variant} 34 + data-variant={status} 35 35 {...props} 36 36 > 37 37 {title}
apps/web/public/assets/changelog/grouped-monitors.png

This is a binary file and will not be displayed.

+8
apps/web/src/content/changelog/grouped-monitors.mdx
··· 1 + --- 2 + title: Grouped Monitors 3 + description: Organize monitors by region or service for a cleaner overview. 4 + image: /assets/changelog/grouped-monitors.png 5 + publishedAt: 2025-11-06 6 + --- 7 + 8 + You can now group monitors on your status page - for example, by region (EU, US, APAC) or service (API, Dashboard, Auth). This makes it easier for visitors to understand which parts of your system are affected.
+64 -7
packages/api/src/router/page.ts
··· 18 18 legacy_selectPublicPageSchemaWithRelation, 19 19 maintenance, 20 20 monitor, 21 + monitorGroup, 21 22 monitorsToPages, 22 23 page, 23 24 selectMaintenanceSchema, 25 + selectMonitorGroupSchema, 24 26 selectMonitorSchema, 25 27 selectPageSchema, 26 28 selectPageSchemaWithMonitorsRelation, ··· 490 492 const data = await opts.ctx.db.query.page.findFirst({ 491 493 where: and(...whereConditions), 492 494 with: { 493 - monitorsToPages: { with: { monitor: true } }, 495 + monitorsToPages: { with: { monitor: true, monitorGroup: true } }, 494 496 maintenances: true, 495 497 }, 496 498 }); ··· 498 500 return selectPageSchema 499 501 .extend({ 500 502 monitors: z 501 - .array(selectMonitorSchema.extend({ order: z.number().default(0) })) 503 + .array( 504 + selectMonitorSchema.extend({ 505 + order: z.number().default(0), 506 + groupOrder: z.number().default(0), 507 + groupId: z.number().nullable(), 508 + }), 509 + ) 502 510 .default([]), 511 + monitorGroups: z.array(selectMonitorGroupSchema).default([]), 503 512 maintenances: z.array(selectMaintenanceSchema).default([]), 504 513 }) 505 514 .parse({ ··· 507 516 monitors: data?.monitorsToPages.map((m) => ({ 508 517 ...m.monitor, 509 518 order: m.order, 519 + groupId: m.monitorGroupId, 520 + groupOrder: m.groupOrder, 510 521 })), 522 + monitorGroups: Array.from( 523 + new Map( 524 + data?.monitorsToPages 525 + .filter((m) => m.monitorGroup) 526 + .map((m) => [m.monitorGroup?.id, m.monitorGroup]), 527 + ).values(), 528 + ), 511 529 maintenances: data?.maintenances, 512 530 }); 513 531 }), ··· 824 842 z.object({ 825 843 id: z.number(), 826 844 monitors: z.array(z.object({ id: z.number(), order: z.number() })), 845 + groups: z.array( 846 + z.object({ 847 + // id: z.number(), // we dont need it as we are deleting and adding 848 + order: z.number(), 849 + name: z.string(), 850 + monitors: z.array(z.object({ id: z.number(), order: z.number() })), 851 + }), 852 + ), 827 853 }), 828 854 ) 829 855 .mutation(async (opts) => { 856 + const monitorIds = opts.input.monitors.map((m) => m.id); 857 + const groupMonitorIds = opts.input.groups.flatMap((g) => 858 + g.monitors.map((m) => m.id), 859 + ); 860 + 861 + const allMonitorIds = [...new Set([...monitorIds, ...groupMonitorIds])]; 830 862 // check if the monitors are in the workspace 831 863 const monitors = await opts.ctx.db.query.monitor.findMany({ 832 864 where: and( 833 - inArray( 834 - monitor.id, 835 - opts.input.monitors.map((m) => m.id), 836 - ), 865 + inArray(monitor.id, allMonitorIds), 837 866 eq(monitor.workspaceId, opts.ctx.workspace.id), 838 867 ), 839 868 }); 840 869 841 - if (monitors.length !== opts.input.monitors.length) { 870 + if (monitors.length !== allMonitorIds.length) { 842 871 throw new TRPCError({ 843 872 code: "FORBIDDEN", 844 873 message: "You don't have access to all the monitors.", ··· 847 876 848 877 await opts.ctx.db.transaction(async (tx) => { 849 878 await tx 879 + .delete(monitorGroup) 880 + .where(eq(monitorGroup.pageId, opts.input.id)); 881 + await tx 850 882 .delete(monitorsToPages) 851 883 .where(eq(monitorsToPages.pageId, opts.input.id)); 884 + 885 + if (opts.input.groups.length > 0) { 886 + const monitorGroups = await tx 887 + .insert(monitorGroup) 888 + .values( 889 + opts.input.groups.map((g) => ({ 890 + workspaceId: opts.ctx.workspace.id, 891 + pageId: opts.input.id, 892 + name: g.name, 893 + })), 894 + ) 895 + .returning(); 896 + 897 + await tx.insert(monitorsToPages).values( 898 + opts.input.groups.flatMap((g, i) => 899 + g.monitors.map((m) => ({ 900 + pageId: opts.input.id, 901 + monitorId: m.id, 902 + order: g.order, 903 + monitorGroupId: monitorGroups[i].id, 904 + groupOrder: m.order, 905 + })), 906 + ), 907 + ); 908 + } 852 909 853 910 if (opts.input.monitors.length > 0) { 854 911 await tx.insert(monitorsToPages).values(
+110 -1
packages/api/src/router/statusPage.ts
··· 21 21 fillStatusDataFor45DaysNoop, 22 22 getEvents, 23 23 getUptime, 24 + getWorstVariant, 24 25 setDataByType, 25 26 } from "./statusPage.utils"; 26 27 import { ··· 82 83 incidents: true, 83 84 }, 84 85 }, 86 + monitorGroup: true, 85 87 }, 86 88 orderBy: (monitorsToPages, { asc }) => asc(monitorsToPages.order), 87 89 }, ··· 127 129 ) 128 130 ? "info" 129 131 : "success"; 130 - return { ...m.monitor, status, events }; 132 + return { 133 + ...m.monitor, 134 + status, 135 + events, 136 + monitorGroupId: m.monitorGroupId, 137 + order: m.order, 138 + groupOrder: m.groupOrder, 139 + }; 131 140 }); 132 141 133 142 const status = ··· 177 186 return false; 178 187 }); 179 188 189 + const monitorGroups = Array.from( 190 + new Map( 191 + _page.monitorsToPages.map((m) => [ 192 + m.monitorGroup?.id, 193 + m.monitorGroup, 194 + ]), 195 + ) 196 + .values() 197 + .filter(Boolean), 198 + ); 199 + 200 + // Create trackers array with grouped and ungrouped monitors 201 + const groupedMap = new Map< 202 + number | null, 203 + { 204 + groupId: number | null; 205 + groupName: string | null; 206 + monitors: typeof monitors; 207 + minOrder: number; 208 + } 209 + >(); 210 + 211 + monitors.forEach((monitor) => { 212 + const groupId = monitor.monitorGroupId ?? null; 213 + const group = groupId 214 + ? monitorGroups.find((g) => g?.id === groupId) 215 + : null; 216 + const groupName = group?.name ?? null; 217 + 218 + if (!groupedMap.has(groupId)) { 219 + groupedMap.set(groupId, { 220 + groupId, 221 + groupName, 222 + monitors: [], 223 + minOrder: monitor.order ?? 0, 224 + }); 225 + } 226 + const currentGroup = groupedMap.get(groupId); 227 + if (currentGroup) { 228 + currentGroup.monitors.push(monitor); 229 + currentGroup.minOrder = Math.min( 230 + currentGroup.minOrder, 231 + monitor.order ?? 0, 232 + ); 233 + } 234 + }); 235 + 236 + // Convert to trackers array 237 + type MonitorTracker = { 238 + type: "monitor"; 239 + monitor: (typeof monitors)[number]; 240 + order: number; 241 + }; 242 + 243 + type GroupTracker = { 244 + type: "group"; 245 + groupId: number; 246 + groupName: string; 247 + monitors: typeof monitors; 248 + status: "success" | "degraded" | "error" | "info" | "empty"; 249 + order: number; 250 + }; 251 + 252 + type Tracker = MonitorTracker | GroupTracker; 253 + 254 + const trackers: Tracker[] = Array.from(groupedMap.values()) 255 + .flatMap((group): Tracker[] => { 256 + if (group.groupId === null) { 257 + // Ungrouped monitors - return as individual trackers 258 + return group.monitors.map( 259 + (monitor): MonitorTracker => ({ 260 + type: "monitor", 261 + monitor, 262 + order: monitor.order ?? 0, 263 + }), 264 + ); 265 + } 266 + // Grouped monitors - return as single group tracker 267 + const sortedMonitors = group.monitors.sort( 268 + (a, b) => (a.groupOrder ?? 0) - (b.groupOrder ?? 0), 269 + ); 270 + return [ 271 + { 272 + type: "group", 273 + groupId: group.groupId, 274 + groupName: group.groupName ?? "", 275 + monitors: sortedMonitors, 276 + status: getWorstVariant( 277 + group.monitors.map( 278 + (m) => m.status as "success" | "degraded" | "error" | "info", 279 + ), 280 + ), 281 + order: group.minOrder, 282 + }, 283 + ]; 284 + }) 285 + .sort((a, b) => a.order - b.order); 286 + 180 287 return selectPublicPageSchemaWithRelation.parse({ 181 288 ..._page, 182 289 monitors, 290 + monitorGroups, 291 + trackers, 183 292 incidents: monitors.flatMap((m) => m.incidents) ?? [], 184 293 statusReports: 185 294 // NOTE: we need to sort the status reports by the first update date
+15
packages/api/src/router/statusPage.utils.ts
··· 204 204 // Keep the old function name for backward compatibility 205 205 export const getEventsByMonitorId = getEvents; 206 206 207 + export function getWorstVariant( 208 + statuses: (keyof typeof STATUS_PRIORITY)[], 209 + ): keyof typeof STATUS_PRIORITY { 210 + if (statuses.length === 0) return "success"; 211 + 212 + return statuses.reduce( 213 + (worst, current) => { 214 + return STATUS_PRIORITY[current] > STATUS_PRIORITY[worst] 215 + ? current 216 + : worst; 217 + }, 218 + "success" as keyof typeof STATUS_PRIORITY, 219 + ); 220 + } 221 + 207 222 type UptimeData = { 208 223 day: string; 209 224 events: Event[];
+13
packages/db/drizzle/0049_sloppy_inhumans.sql
··· 1 + CREATE TABLE `monitor_group` ( 2 + `id` integer PRIMARY KEY NOT NULL, 3 + `workspace_id` integer NOT NULL, 4 + `page_id` integer NOT NULL, 5 + `name` text NOT NULL, 6 + `created_at` integer DEFAULT (strftime('%s', 'now')), 7 + `updated_at` integer DEFAULT (strftime('%s', 'now')), 8 + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE cascade, 9 + FOREIGN KEY (`page_id`) REFERENCES `page`(`id`) ON UPDATE no action ON DELETE cascade 10 + ); 11 + --> statement-breakpoint 12 + ALTER TABLE `monitors_to_pages` ADD `monitor_group_id` integer REFERENCES monitor_group(id);--> statement-breakpoint 13 + ALTER TABLE `monitors_to_pages` ADD `group_order` integer DEFAULT 0;
+2696
packages/db/drizzle/meta/0049_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "7fd60946-e536-4fb1-863b-877d0d01dcc9", 5 + "prevId": "091d5fb3-ae4f-4ce2-a91f-695d054c0f79", 6 + "tables": { 7 + "workspace": { 8 + "name": "workspace", 9 + "columns": { 10 + "id": { 11 + "name": "id", 12 + "type": "integer", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "slug": { 18 + "name": "slug", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "name": { 25 + "name": "name", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": false, 29 + "autoincrement": false 30 + }, 31 + "stripe_id": { 32 + "name": "stripe_id", 33 + "type": "text(256)", 34 + "primaryKey": false, 35 + "notNull": false, 36 + "autoincrement": false 37 + }, 38 + "subscription_id": { 39 + "name": "subscription_id", 40 + "type": "text", 41 + "primaryKey": false, 42 + "notNull": false, 43 + "autoincrement": false 44 + }, 45 + "plan": { 46 + "name": "plan", 47 + "type": "text", 48 + "primaryKey": false, 49 + "notNull": false, 50 + "autoincrement": false 51 + }, 52 + "ends_at": { 53 + "name": "ends_at", 54 + "type": "integer", 55 + "primaryKey": false, 56 + "notNull": false, 57 + "autoincrement": false 58 + }, 59 + "paid_until": { 60 + "name": "paid_until", 61 + "type": "integer", 62 + "primaryKey": false, 63 + "notNull": false, 64 + "autoincrement": false 65 + }, 66 + "limits": { 67 + "name": "limits", 68 + "type": "text", 69 + "primaryKey": false, 70 + "notNull": true, 71 + "autoincrement": false, 72 + "default": "'{}'" 73 + }, 74 + "created_at": { 75 + "name": "created_at", 76 + "type": "integer", 77 + "primaryKey": false, 78 + "notNull": false, 79 + "autoincrement": false, 80 + "default": "(strftime('%s', 'now'))" 81 + }, 82 + "updated_at": { 83 + "name": "updated_at", 84 + "type": "integer", 85 + "primaryKey": false, 86 + "notNull": false, 87 + "autoincrement": false, 88 + "default": "(strftime('%s', 'now'))" 89 + }, 90 + "dsn": { 91 + "name": "dsn", 92 + "type": "text", 93 + "primaryKey": false, 94 + "notNull": false, 95 + "autoincrement": false 96 + } 97 + }, 98 + "indexes": { 99 + "workspace_slug_unique": { 100 + "name": "workspace_slug_unique", 101 + "columns": [ 102 + "slug" 103 + ], 104 + "isUnique": true 105 + }, 106 + "workspace_stripe_id_unique": { 107 + "name": "workspace_stripe_id_unique", 108 + "columns": [ 109 + "stripe_id" 110 + ], 111 + "isUnique": true 112 + }, 113 + "workspace_id_dsn_unique": { 114 + "name": "workspace_id_dsn_unique", 115 + "columns": [ 116 + "id", 117 + "dsn" 118 + ], 119 + "isUnique": true 120 + } 121 + }, 122 + "foreignKeys": {}, 123 + "compositePrimaryKeys": {}, 124 + "uniqueConstraints": {}, 125 + "checkConstraints": {} 126 + }, 127 + "account": { 128 + "name": "account", 129 + "columns": { 130 + "user_id": { 131 + "name": "user_id", 132 + "type": "integer", 133 + "primaryKey": false, 134 + "notNull": true, 135 + "autoincrement": false 136 + }, 137 + "type": { 138 + "name": "type", 139 + "type": "text", 140 + "primaryKey": false, 141 + "notNull": true, 142 + "autoincrement": false 143 + }, 144 + "provider": { 145 + "name": "provider", 146 + "type": "text", 147 + "primaryKey": false, 148 + "notNull": true, 149 + "autoincrement": false 150 + }, 151 + "provider_account_id": { 152 + "name": "provider_account_id", 153 + "type": "text", 154 + "primaryKey": false, 155 + "notNull": true, 156 + "autoincrement": false 157 + }, 158 + "refresh_token": { 159 + "name": "refresh_token", 160 + "type": "text", 161 + "primaryKey": false, 162 + "notNull": false, 163 + "autoincrement": false 164 + }, 165 + "access_token": { 166 + "name": "access_token", 167 + "type": "text", 168 + "primaryKey": false, 169 + "notNull": false, 170 + "autoincrement": false 171 + }, 172 + "expires_at": { 173 + "name": "expires_at", 174 + "type": "integer", 175 + "primaryKey": false, 176 + "notNull": false, 177 + "autoincrement": false 178 + }, 179 + "token_type": { 180 + "name": "token_type", 181 + "type": "text", 182 + "primaryKey": false, 183 + "notNull": false, 184 + "autoincrement": false 185 + }, 186 + "scope": { 187 + "name": "scope", 188 + "type": "text", 189 + "primaryKey": false, 190 + "notNull": false, 191 + "autoincrement": false 192 + }, 193 + "id_token": { 194 + "name": "id_token", 195 + "type": "text", 196 + "primaryKey": false, 197 + "notNull": false, 198 + "autoincrement": false 199 + }, 200 + "session_state": { 201 + "name": "session_state", 202 + "type": "text", 203 + "primaryKey": false, 204 + "notNull": false, 205 + "autoincrement": false 206 + } 207 + }, 208 + "indexes": {}, 209 + "foreignKeys": { 210 + "account_user_id_user_id_fk": { 211 + "name": "account_user_id_user_id_fk", 212 + "tableFrom": "account", 213 + "tableTo": "user", 214 + "columnsFrom": [ 215 + "user_id" 216 + ], 217 + "columnsTo": [ 218 + "id" 219 + ], 220 + "onDelete": "cascade", 221 + "onUpdate": "no action" 222 + } 223 + }, 224 + "compositePrimaryKeys": { 225 + "account_provider_provider_account_id_pk": { 226 + "columns": [ 227 + "provider", 228 + "provider_account_id" 229 + ], 230 + "name": "account_provider_provider_account_id_pk" 231 + } 232 + }, 233 + "uniqueConstraints": {}, 234 + "checkConstraints": {} 235 + }, 236 + "session": { 237 + "name": "session", 238 + "columns": { 239 + "session_token": { 240 + "name": "session_token", 241 + "type": "text", 242 + "primaryKey": true, 243 + "notNull": true, 244 + "autoincrement": false 245 + }, 246 + "user_id": { 247 + "name": "user_id", 248 + "type": "integer", 249 + "primaryKey": false, 250 + "notNull": true, 251 + "autoincrement": false 252 + }, 253 + "expires": { 254 + "name": "expires", 255 + "type": "integer", 256 + "primaryKey": false, 257 + "notNull": true, 258 + "autoincrement": false 259 + } 260 + }, 261 + "indexes": {}, 262 + "foreignKeys": { 263 + "session_user_id_user_id_fk": { 264 + "name": "session_user_id_user_id_fk", 265 + "tableFrom": "session", 266 + "tableTo": "user", 267 + "columnsFrom": [ 268 + "user_id" 269 + ], 270 + "columnsTo": [ 271 + "id" 272 + ], 273 + "onDelete": "cascade", 274 + "onUpdate": "no action" 275 + } 276 + }, 277 + "compositePrimaryKeys": {}, 278 + "uniqueConstraints": {}, 279 + "checkConstraints": {} 280 + }, 281 + "user": { 282 + "name": "user", 283 + "columns": { 284 + "id": { 285 + "name": "id", 286 + "type": "integer", 287 + "primaryKey": true, 288 + "notNull": true, 289 + "autoincrement": false 290 + }, 291 + "tenant_id": { 292 + "name": "tenant_id", 293 + "type": "text(256)", 294 + "primaryKey": false, 295 + "notNull": false, 296 + "autoincrement": false 297 + }, 298 + "first_name": { 299 + "name": "first_name", 300 + "type": "text", 301 + "primaryKey": false, 302 + "notNull": false, 303 + "autoincrement": false, 304 + "default": "''" 305 + }, 306 + "last_name": { 307 + "name": "last_name", 308 + "type": "text", 309 + "primaryKey": false, 310 + "notNull": false, 311 + "autoincrement": false, 312 + "default": "''" 313 + }, 314 + "photo_url": { 315 + "name": "photo_url", 316 + "type": "text", 317 + "primaryKey": false, 318 + "notNull": false, 319 + "autoincrement": false, 320 + "default": "''" 321 + }, 322 + "name": { 323 + "name": "name", 324 + "type": "text", 325 + "primaryKey": false, 326 + "notNull": false, 327 + "autoincrement": false 328 + }, 329 + "email": { 330 + "name": "email", 331 + "type": "text", 332 + "primaryKey": false, 333 + "notNull": false, 334 + "autoincrement": false, 335 + "default": "''" 336 + }, 337 + "emailVerified": { 338 + "name": "emailVerified", 339 + "type": "integer", 340 + "primaryKey": false, 341 + "notNull": false, 342 + "autoincrement": false 343 + }, 344 + "created_at": { 345 + "name": "created_at", 346 + "type": "integer", 347 + "primaryKey": false, 348 + "notNull": false, 349 + "autoincrement": false, 350 + "default": "(strftime('%s', 'now'))" 351 + }, 352 + "updated_at": { 353 + "name": "updated_at", 354 + "type": "integer", 355 + "primaryKey": false, 356 + "notNull": false, 357 + "autoincrement": false, 358 + "default": "(strftime('%s', 'now'))" 359 + } 360 + }, 361 + "indexes": { 362 + "user_tenant_id_unique": { 363 + "name": "user_tenant_id_unique", 364 + "columns": [ 365 + "tenant_id" 366 + ], 367 + "isUnique": true 368 + } 369 + }, 370 + "foreignKeys": {}, 371 + "compositePrimaryKeys": {}, 372 + "uniqueConstraints": {}, 373 + "checkConstraints": {} 374 + }, 375 + "users_to_workspaces": { 376 + "name": "users_to_workspaces", 377 + "columns": { 378 + "user_id": { 379 + "name": "user_id", 380 + "type": "integer", 381 + "primaryKey": false, 382 + "notNull": true, 383 + "autoincrement": false 384 + }, 385 + "workspace_id": { 386 + "name": "workspace_id", 387 + "type": "integer", 388 + "primaryKey": false, 389 + "notNull": true, 390 + "autoincrement": false 391 + }, 392 + "role": { 393 + "name": "role", 394 + "type": "text", 395 + "primaryKey": false, 396 + "notNull": true, 397 + "autoincrement": false, 398 + "default": "'member'" 399 + }, 400 + "created_at": { 401 + "name": "created_at", 402 + "type": "integer", 403 + "primaryKey": false, 404 + "notNull": false, 405 + "autoincrement": false, 406 + "default": "(strftime('%s', 'now'))" 407 + } 408 + }, 409 + "indexes": {}, 410 + "foreignKeys": { 411 + "users_to_workspaces_user_id_user_id_fk": { 412 + "name": "users_to_workspaces_user_id_user_id_fk", 413 + "tableFrom": "users_to_workspaces", 414 + "tableTo": "user", 415 + "columnsFrom": [ 416 + "user_id" 417 + ], 418 + "columnsTo": [ 419 + "id" 420 + ], 421 + "onDelete": "no action", 422 + "onUpdate": "no action" 423 + }, 424 + "users_to_workspaces_workspace_id_workspace_id_fk": { 425 + "name": "users_to_workspaces_workspace_id_workspace_id_fk", 426 + "tableFrom": "users_to_workspaces", 427 + "tableTo": "workspace", 428 + "columnsFrom": [ 429 + "workspace_id" 430 + ], 431 + "columnsTo": [ 432 + "id" 433 + ], 434 + "onDelete": "no action", 435 + "onUpdate": "no action" 436 + } 437 + }, 438 + "compositePrimaryKeys": { 439 + "users_to_workspaces_user_id_workspace_id_pk": { 440 + "columns": [ 441 + "user_id", 442 + "workspace_id" 443 + ], 444 + "name": "users_to_workspaces_user_id_workspace_id_pk" 445 + } 446 + }, 447 + "uniqueConstraints": {}, 448 + "checkConstraints": {} 449 + }, 450 + "verification_token": { 451 + "name": "verification_token", 452 + "columns": { 453 + "identifier": { 454 + "name": "identifier", 455 + "type": "text", 456 + "primaryKey": false, 457 + "notNull": true, 458 + "autoincrement": false 459 + }, 460 + "token": { 461 + "name": "token", 462 + "type": "text", 463 + "primaryKey": false, 464 + "notNull": true, 465 + "autoincrement": false 466 + }, 467 + "expires": { 468 + "name": "expires", 469 + "type": "integer", 470 + "primaryKey": false, 471 + "notNull": true, 472 + "autoincrement": false 473 + } 474 + }, 475 + "indexes": {}, 476 + "foreignKeys": {}, 477 + "compositePrimaryKeys": { 478 + "verification_token_identifier_token_pk": { 479 + "columns": [ 480 + "identifier", 481 + "token" 482 + ], 483 + "name": "verification_token_identifier_token_pk" 484 + } 485 + }, 486 + "uniqueConstraints": {}, 487 + "checkConstraints": {} 488 + }, 489 + "status_report_to_monitors": { 490 + "name": "status_report_to_monitors", 491 + "columns": { 492 + "monitor_id": { 493 + "name": "monitor_id", 494 + "type": "integer", 495 + "primaryKey": false, 496 + "notNull": true, 497 + "autoincrement": false 498 + }, 499 + "status_report_id": { 500 + "name": "status_report_id", 501 + "type": "integer", 502 + "primaryKey": false, 503 + "notNull": true, 504 + "autoincrement": false 505 + }, 506 + "created_at": { 507 + "name": "created_at", 508 + "type": "integer", 509 + "primaryKey": false, 510 + "notNull": false, 511 + "autoincrement": false, 512 + "default": "(strftime('%s', 'now'))" 513 + } 514 + }, 515 + "indexes": {}, 516 + "foreignKeys": { 517 + "status_report_to_monitors_monitor_id_monitor_id_fk": { 518 + "name": "status_report_to_monitors_monitor_id_monitor_id_fk", 519 + "tableFrom": "status_report_to_monitors", 520 + "tableTo": "monitor", 521 + "columnsFrom": [ 522 + "monitor_id" 523 + ], 524 + "columnsTo": [ 525 + "id" 526 + ], 527 + "onDelete": "cascade", 528 + "onUpdate": "no action" 529 + }, 530 + "status_report_to_monitors_status_report_id_status_report_id_fk": { 531 + "name": "status_report_to_monitors_status_report_id_status_report_id_fk", 532 + "tableFrom": "status_report_to_monitors", 533 + "tableTo": "status_report", 534 + "columnsFrom": [ 535 + "status_report_id" 536 + ], 537 + "columnsTo": [ 538 + "id" 539 + ], 540 + "onDelete": "cascade", 541 + "onUpdate": "no action" 542 + } 543 + }, 544 + "compositePrimaryKeys": { 545 + "status_report_to_monitors_monitor_id_status_report_id_pk": { 546 + "columns": [ 547 + "monitor_id", 548 + "status_report_id" 549 + ], 550 + "name": "status_report_to_monitors_monitor_id_status_report_id_pk" 551 + } 552 + }, 553 + "uniqueConstraints": {}, 554 + "checkConstraints": {} 555 + }, 556 + "status_report": { 557 + "name": "status_report", 558 + "columns": { 559 + "id": { 560 + "name": "id", 561 + "type": "integer", 562 + "primaryKey": true, 563 + "notNull": true, 564 + "autoincrement": false 565 + }, 566 + "status": { 567 + "name": "status", 568 + "type": "text", 569 + "primaryKey": false, 570 + "notNull": true, 571 + "autoincrement": false 572 + }, 573 + "title": { 574 + "name": "title", 575 + "type": "text(256)", 576 + "primaryKey": false, 577 + "notNull": true, 578 + "autoincrement": false 579 + }, 580 + "workspace_id": { 581 + "name": "workspace_id", 582 + "type": "integer", 583 + "primaryKey": false, 584 + "notNull": false, 585 + "autoincrement": false 586 + }, 587 + "page_id": { 588 + "name": "page_id", 589 + "type": "integer", 590 + "primaryKey": false, 591 + "notNull": false, 592 + "autoincrement": false 593 + }, 594 + "created_at": { 595 + "name": "created_at", 596 + "type": "integer", 597 + "primaryKey": false, 598 + "notNull": false, 599 + "autoincrement": false, 600 + "default": "(strftime('%s', 'now'))" 601 + }, 602 + "updated_at": { 603 + "name": "updated_at", 604 + "type": "integer", 605 + "primaryKey": false, 606 + "notNull": false, 607 + "autoincrement": false, 608 + "default": "(strftime('%s', 'now'))" 609 + } 610 + }, 611 + "indexes": {}, 612 + "foreignKeys": { 613 + "status_report_workspace_id_workspace_id_fk": { 614 + "name": "status_report_workspace_id_workspace_id_fk", 615 + "tableFrom": "status_report", 616 + "tableTo": "workspace", 617 + "columnsFrom": [ 618 + "workspace_id" 619 + ], 620 + "columnsTo": [ 621 + "id" 622 + ], 623 + "onDelete": "no action", 624 + "onUpdate": "no action" 625 + }, 626 + "status_report_page_id_page_id_fk": { 627 + "name": "status_report_page_id_page_id_fk", 628 + "tableFrom": "status_report", 629 + "tableTo": "page", 630 + "columnsFrom": [ 631 + "page_id" 632 + ], 633 + "columnsTo": [ 634 + "id" 635 + ], 636 + "onDelete": "cascade", 637 + "onUpdate": "no action" 638 + } 639 + }, 640 + "compositePrimaryKeys": {}, 641 + "uniqueConstraints": {}, 642 + "checkConstraints": {} 643 + }, 644 + "status_report_update": { 645 + "name": "status_report_update", 646 + "columns": { 647 + "id": { 648 + "name": "id", 649 + "type": "integer", 650 + "primaryKey": true, 651 + "notNull": true, 652 + "autoincrement": false 653 + }, 654 + "status": { 655 + "name": "status", 656 + "type": "text", 657 + "primaryKey": false, 658 + "notNull": true, 659 + "autoincrement": false 660 + }, 661 + "date": { 662 + "name": "date", 663 + "type": "integer", 664 + "primaryKey": false, 665 + "notNull": true, 666 + "autoincrement": false 667 + }, 668 + "message": { 669 + "name": "message", 670 + "type": "text", 671 + "primaryKey": false, 672 + "notNull": true, 673 + "autoincrement": false 674 + }, 675 + "status_report_id": { 676 + "name": "status_report_id", 677 + "type": "integer", 678 + "primaryKey": false, 679 + "notNull": true, 680 + "autoincrement": false 681 + }, 682 + "created_at": { 683 + "name": "created_at", 684 + "type": "integer", 685 + "primaryKey": false, 686 + "notNull": false, 687 + "autoincrement": false, 688 + "default": "(strftime('%s', 'now'))" 689 + }, 690 + "updated_at": { 691 + "name": "updated_at", 692 + "type": "integer", 693 + "primaryKey": false, 694 + "notNull": false, 695 + "autoincrement": false, 696 + "default": "(strftime('%s', 'now'))" 697 + } 698 + }, 699 + "indexes": {}, 700 + "foreignKeys": { 701 + "status_report_update_status_report_id_status_report_id_fk": { 702 + "name": "status_report_update_status_report_id_status_report_id_fk", 703 + "tableFrom": "status_report_update", 704 + "tableTo": "status_report", 705 + "columnsFrom": [ 706 + "status_report_id" 707 + ], 708 + "columnsTo": [ 709 + "id" 710 + ], 711 + "onDelete": "cascade", 712 + "onUpdate": "no action" 713 + } 714 + }, 715 + "compositePrimaryKeys": {}, 716 + "uniqueConstraints": {}, 717 + "checkConstraints": {} 718 + }, 719 + "integration": { 720 + "name": "integration", 721 + "columns": { 722 + "id": { 723 + "name": "id", 724 + "type": "integer", 725 + "primaryKey": true, 726 + "notNull": true, 727 + "autoincrement": false 728 + }, 729 + "name": { 730 + "name": "name", 731 + "type": "text(256)", 732 + "primaryKey": false, 733 + "notNull": true, 734 + "autoincrement": false 735 + }, 736 + "workspace_id": { 737 + "name": "workspace_id", 738 + "type": "integer", 739 + "primaryKey": false, 740 + "notNull": false, 741 + "autoincrement": false 742 + }, 743 + "credential": { 744 + "name": "credential", 745 + "type": "text", 746 + "primaryKey": false, 747 + "notNull": false, 748 + "autoincrement": false 749 + }, 750 + "external_id": { 751 + "name": "external_id", 752 + "type": "text", 753 + "primaryKey": false, 754 + "notNull": true, 755 + "autoincrement": false 756 + }, 757 + "created_at": { 758 + "name": "created_at", 759 + "type": "integer", 760 + "primaryKey": false, 761 + "notNull": false, 762 + "autoincrement": false, 763 + "default": "(strftime('%s', 'now'))" 764 + }, 765 + "updated_at": { 766 + "name": "updated_at", 767 + "type": "integer", 768 + "primaryKey": false, 769 + "notNull": false, 770 + "autoincrement": false, 771 + "default": "(strftime('%s', 'now'))" 772 + }, 773 + "data": { 774 + "name": "data", 775 + "type": "text", 776 + "primaryKey": false, 777 + "notNull": true, 778 + "autoincrement": false 779 + } 780 + }, 781 + "indexes": {}, 782 + "foreignKeys": { 783 + "integration_workspace_id_workspace_id_fk": { 784 + "name": "integration_workspace_id_workspace_id_fk", 785 + "tableFrom": "integration", 786 + "tableTo": "workspace", 787 + "columnsFrom": [ 788 + "workspace_id" 789 + ], 790 + "columnsTo": [ 791 + "id" 792 + ], 793 + "onDelete": "no action", 794 + "onUpdate": "no action" 795 + } 796 + }, 797 + "compositePrimaryKeys": {}, 798 + "uniqueConstraints": {}, 799 + "checkConstraints": {} 800 + }, 801 + "page": { 802 + "name": "page", 803 + "columns": { 804 + "id": { 805 + "name": "id", 806 + "type": "integer", 807 + "primaryKey": true, 808 + "notNull": true, 809 + "autoincrement": false 810 + }, 811 + "workspace_id": { 812 + "name": "workspace_id", 813 + "type": "integer", 814 + "primaryKey": false, 815 + "notNull": true, 816 + "autoincrement": false 817 + }, 818 + "title": { 819 + "name": "title", 820 + "type": "text", 821 + "primaryKey": false, 822 + "notNull": true, 823 + "autoincrement": false 824 + }, 825 + "description": { 826 + "name": "description", 827 + "type": "text", 828 + "primaryKey": false, 829 + "notNull": true, 830 + "autoincrement": false 831 + }, 832 + "icon": { 833 + "name": "icon", 834 + "type": "text(256)", 835 + "primaryKey": false, 836 + "notNull": false, 837 + "autoincrement": false, 838 + "default": "''" 839 + }, 840 + "slug": { 841 + "name": "slug", 842 + "type": "text(256)", 843 + "primaryKey": false, 844 + "notNull": true, 845 + "autoincrement": false 846 + }, 847 + "custom_domain": { 848 + "name": "custom_domain", 849 + "type": "text(256)", 850 + "primaryKey": false, 851 + "notNull": true, 852 + "autoincrement": false 853 + }, 854 + "published": { 855 + "name": "published", 856 + "type": "integer", 857 + "primaryKey": false, 858 + "notNull": false, 859 + "autoincrement": false, 860 + "default": false 861 + }, 862 + "force_theme": { 863 + "name": "force_theme", 864 + "type": "text", 865 + "primaryKey": false, 866 + "notNull": true, 867 + "autoincrement": false, 868 + "default": "'system'" 869 + }, 870 + "password": { 871 + "name": "password", 872 + "type": "text(256)", 873 + "primaryKey": false, 874 + "notNull": false, 875 + "autoincrement": false 876 + }, 877 + "password_protected": { 878 + "name": "password_protected", 879 + "type": "integer", 880 + "primaryKey": false, 881 + "notNull": false, 882 + "autoincrement": false, 883 + "default": false 884 + }, 885 + "homepage_url": { 886 + "name": "homepage_url", 887 + "type": "text(256)", 888 + "primaryKey": false, 889 + "notNull": false, 890 + "autoincrement": false 891 + }, 892 + "contact_url": { 893 + "name": "contact_url", 894 + "type": "text(256)", 895 + "primaryKey": false, 896 + "notNull": false, 897 + "autoincrement": false 898 + }, 899 + "legacy_page": { 900 + "name": "legacy_page", 901 + "type": "integer", 902 + "primaryKey": false, 903 + "notNull": true, 904 + "autoincrement": false, 905 + "default": true 906 + }, 907 + "configuration": { 908 + "name": "configuration", 909 + "type": "text", 910 + "primaryKey": false, 911 + "notNull": false, 912 + "autoincrement": false 913 + }, 914 + "show_monitor_values": { 915 + "name": "show_monitor_values", 916 + "type": "integer", 917 + "primaryKey": false, 918 + "notNull": false, 919 + "autoincrement": false, 920 + "default": true 921 + }, 922 + "created_at": { 923 + "name": "created_at", 924 + "type": "integer", 925 + "primaryKey": false, 926 + "notNull": false, 927 + "autoincrement": false, 928 + "default": "(strftime('%s', 'now'))" 929 + }, 930 + "updated_at": { 931 + "name": "updated_at", 932 + "type": "integer", 933 + "primaryKey": false, 934 + "notNull": false, 935 + "autoincrement": false, 936 + "default": "(strftime('%s', 'now'))" 937 + } 938 + }, 939 + "indexes": { 940 + "page_slug_unique": { 941 + "name": "page_slug_unique", 942 + "columns": [ 943 + "slug" 944 + ], 945 + "isUnique": true 946 + } 947 + }, 948 + "foreignKeys": { 949 + "page_workspace_id_workspace_id_fk": { 950 + "name": "page_workspace_id_workspace_id_fk", 951 + "tableFrom": "page", 952 + "tableTo": "workspace", 953 + "columnsFrom": [ 954 + "workspace_id" 955 + ], 956 + "columnsTo": [ 957 + "id" 958 + ], 959 + "onDelete": "cascade", 960 + "onUpdate": "no action" 961 + } 962 + }, 963 + "compositePrimaryKeys": {}, 964 + "uniqueConstraints": {}, 965 + "checkConstraints": {} 966 + }, 967 + "monitor": { 968 + "name": "monitor", 969 + "columns": { 970 + "id": { 971 + "name": "id", 972 + "type": "integer", 973 + "primaryKey": true, 974 + "notNull": true, 975 + "autoincrement": false 976 + }, 977 + "job_type": { 978 + "name": "job_type", 979 + "type": "text", 980 + "primaryKey": false, 981 + "notNull": true, 982 + "autoincrement": false, 983 + "default": "'http'" 984 + }, 985 + "periodicity": { 986 + "name": "periodicity", 987 + "type": "text", 988 + "primaryKey": false, 989 + "notNull": true, 990 + "autoincrement": false, 991 + "default": "'other'" 992 + }, 993 + "status": { 994 + "name": "status", 995 + "type": "text", 996 + "primaryKey": false, 997 + "notNull": true, 998 + "autoincrement": false, 999 + "default": "'active'" 1000 + }, 1001 + "active": { 1002 + "name": "active", 1003 + "type": "integer", 1004 + "primaryKey": false, 1005 + "notNull": false, 1006 + "autoincrement": false, 1007 + "default": false 1008 + }, 1009 + "regions": { 1010 + "name": "regions", 1011 + "type": "text", 1012 + "primaryKey": false, 1013 + "notNull": true, 1014 + "autoincrement": false, 1015 + "default": "''" 1016 + }, 1017 + "url": { 1018 + "name": "url", 1019 + "type": "text(2048)", 1020 + "primaryKey": false, 1021 + "notNull": true, 1022 + "autoincrement": false 1023 + }, 1024 + "name": { 1025 + "name": "name", 1026 + "type": "text(256)", 1027 + "primaryKey": false, 1028 + "notNull": true, 1029 + "autoincrement": false, 1030 + "default": "''" 1031 + }, 1032 + "description": { 1033 + "name": "description", 1034 + "type": "text", 1035 + "primaryKey": false, 1036 + "notNull": true, 1037 + "autoincrement": false, 1038 + "default": "''" 1039 + }, 1040 + "headers": { 1041 + "name": "headers", 1042 + "type": "text", 1043 + "primaryKey": false, 1044 + "notNull": false, 1045 + "autoincrement": false, 1046 + "default": "''" 1047 + }, 1048 + "body": { 1049 + "name": "body", 1050 + "type": "text", 1051 + "primaryKey": false, 1052 + "notNull": false, 1053 + "autoincrement": false, 1054 + "default": "''" 1055 + }, 1056 + "method": { 1057 + "name": "method", 1058 + "type": "text", 1059 + "primaryKey": false, 1060 + "notNull": false, 1061 + "autoincrement": false, 1062 + "default": "'GET'" 1063 + }, 1064 + "workspace_id": { 1065 + "name": "workspace_id", 1066 + "type": "integer", 1067 + "primaryKey": false, 1068 + "notNull": false, 1069 + "autoincrement": false 1070 + }, 1071 + "timeout": { 1072 + "name": "timeout", 1073 + "type": "integer", 1074 + "primaryKey": false, 1075 + "notNull": true, 1076 + "autoincrement": false, 1077 + "default": 45000 1078 + }, 1079 + "degraded_after": { 1080 + "name": "degraded_after", 1081 + "type": "integer", 1082 + "primaryKey": false, 1083 + "notNull": false, 1084 + "autoincrement": false 1085 + }, 1086 + "assertions": { 1087 + "name": "assertions", 1088 + "type": "text", 1089 + "primaryKey": false, 1090 + "notNull": false, 1091 + "autoincrement": false 1092 + }, 1093 + "otel_endpoint": { 1094 + "name": "otel_endpoint", 1095 + "type": "text", 1096 + "primaryKey": false, 1097 + "notNull": false, 1098 + "autoincrement": false 1099 + }, 1100 + "otel_headers": { 1101 + "name": "otel_headers", 1102 + "type": "text", 1103 + "primaryKey": false, 1104 + "notNull": false, 1105 + "autoincrement": false 1106 + }, 1107 + "public": { 1108 + "name": "public", 1109 + "type": "integer", 1110 + "primaryKey": false, 1111 + "notNull": false, 1112 + "autoincrement": false, 1113 + "default": false 1114 + }, 1115 + "retry": { 1116 + "name": "retry", 1117 + "type": "integer", 1118 + "primaryKey": false, 1119 + "notNull": false, 1120 + "autoincrement": false, 1121 + "default": 3 1122 + }, 1123 + "follow_redirects": { 1124 + "name": "follow_redirects", 1125 + "type": "integer", 1126 + "primaryKey": false, 1127 + "notNull": false, 1128 + "autoincrement": false, 1129 + "default": true 1130 + }, 1131 + "created_at": { 1132 + "name": "created_at", 1133 + "type": "integer", 1134 + "primaryKey": false, 1135 + "notNull": false, 1136 + "autoincrement": false, 1137 + "default": "(strftime('%s', 'now'))" 1138 + }, 1139 + "updated_at": { 1140 + "name": "updated_at", 1141 + "type": "integer", 1142 + "primaryKey": false, 1143 + "notNull": false, 1144 + "autoincrement": false, 1145 + "default": "(strftime('%s', 'now'))" 1146 + }, 1147 + "deleted_at": { 1148 + "name": "deleted_at", 1149 + "type": "integer", 1150 + "primaryKey": false, 1151 + "notNull": false, 1152 + "autoincrement": false 1153 + } 1154 + }, 1155 + "indexes": {}, 1156 + "foreignKeys": { 1157 + "monitor_workspace_id_workspace_id_fk": { 1158 + "name": "monitor_workspace_id_workspace_id_fk", 1159 + "tableFrom": "monitor", 1160 + "tableTo": "workspace", 1161 + "columnsFrom": [ 1162 + "workspace_id" 1163 + ], 1164 + "columnsTo": [ 1165 + "id" 1166 + ], 1167 + "onDelete": "no action", 1168 + "onUpdate": "no action" 1169 + } 1170 + }, 1171 + "compositePrimaryKeys": {}, 1172 + "uniqueConstraints": {}, 1173 + "checkConstraints": {} 1174 + }, 1175 + "monitors_to_pages": { 1176 + "name": "monitors_to_pages", 1177 + "columns": { 1178 + "monitor_id": { 1179 + "name": "monitor_id", 1180 + "type": "integer", 1181 + "primaryKey": false, 1182 + "notNull": true, 1183 + "autoincrement": false 1184 + }, 1185 + "page_id": { 1186 + "name": "page_id", 1187 + "type": "integer", 1188 + "primaryKey": false, 1189 + "notNull": true, 1190 + "autoincrement": false 1191 + }, 1192 + "created_at": { 1193 + "name": "created_at", 1194 + "type": "integer", 1195 + "primaryKey": false, 1196 + "notNull": false, 1197 + "autoincrement": false, 1198 + "default": "(strftime('%s', 'now'))" 1199 + }, 1200 + "order": { 1201 + "name": "order", 1202 + "type": "integer", 1203 + "primaryKey": false, 1204 + "notNull": false, 1205 + "autoincrement": false, 1206 + "default": 0 1207 + }, 1208 + "monitor_group_id": { 1209 + "name": "monitor_group_id", 1210 + "type": "integer", 1211 + "primaryKey": false, 1212 + "notNull": false, 1213 + "autoincrement": false 1214 + }, 1215 + "group_order": { 1216 + "name": "group_order", 1217 + "type": "integer", 1218 + "primaryKey": false, 1219 + "notNull": false, 1220 + "autoincrement": false, 1221 + "default": 0 1222 + } 1223 + }, 1224 + "indexes": {}, 1225 + "foreignKeys": { 1226 + "monitors_to_pages_monitor_id_monitor_id_fk": { 1227 + "name": "monitors_to_pages_monitor_id_monitor_id_fk", 1228 + "tableFrom": "monitors_to_pages", 1229 + "tableTo": "monitor", 1230 + "columnsFrom": [ 1231 + "monitor_id" 1232 + ], 1233 + "columnsTo": [ 1234 + "id" 1235 + ], 1236 + "onDelete": "cascade", 1237 + "onUpdate": "no action" 1238 + }, 1239 + "monitors_to_pages_page_id_page_id_fk": { 1240 + "name": "monitors_to_pages_page_id_page_id_fk", 1241 + "tableFrom": "monitors_to_pages", 1242 + "tableTo": "page", 1243 + "columnsFrom": [ 1244 + "page_id" 1245 + ], 1246 + "columnsTo": [ 1247 + "id" 1248 + ], 1249 + "onDelete": "cascade", 1250 + "onUpdate": "no action" 1251 + }, 1252 + "monitors_to_pages_monitor_group_id_monitor_group_id_fk": { 1253 + "name": "monitors_to_pages_monitor_group_id_monitor_group_id_fk", 1254 + "tableFrom": "monitors_to_pages", 1255 + "tableTo": "monitor_group", 1256 + "columnsFrom": [ 1257 + "monitor_group_id" 1258 + ], 1259 + "columnsTo": [ 1260 + "id" 1261 + ], 1262 + "onDelete": "cascade", 1263 + "onUpdate": "no action" 1264 + } 1265 + }, 1266 + "compositePrimaryKeys": { 1267 + "monitors_to_pages_monitor_id_page_id_pk": { 1268 + "columns": [ 1269 + "monitor_id", 1270 + "page_id" 1271 + ], 1272 + "name": "monitors_to_pages_monitor_id_page_id_pk" 1273 + } 1274 + }, 1275 + "uniqueConstraints": {}, 1276 + "checkConstraints": {} 1277 + }, 1278 + "page_subscriber": { 1279 + "name": "page_subscriber", 1280 + "columns": { 1281 + "id": { 1282 + "name": "id", 1283 + "type": "integer", 1284 + "primaryKey": true, 1285 + "notNull": true, 1286 + "autoincrement": false 1287 + }, 1288 + "email": { 1289 + "name": "email", 1290 + "type": "text", 1291 + "primaryKey": false, 1292 + "notNull": true, 1293 + "autoincrement": false 1294 + }, 1295 + "page_id": { 1296 + "name": "page_id", 1297 + "type": "integer", 1298 + "primaryKey": false, 1299 + "notNull": true, 1300 + "autoincrement": false 1301 + }, 1302 + "token": { 1303 + "name": "token", 1304 + "type": "text", 1305 + "primaryKey": false, 1306 + "notNull": false, 1307 + "autoincrement": false 1308 + }, 1309 + "accepted_at": { 1310 + "name": "accepted_at", 1311 + "type": "integer", 1312 + "primaryKey": false, 1313 + "notNull": false, 1314 + "autoincrement": false 1315 + }, 1316 + "expires_at": { 1317 + "name": "expires_at", 1318 + "type": "integer", 1319 + "primaryKey": false, 1320 + "notNull": false, 1321 + "autoincrement": false 1322 + }, 1323 + "created_at": { 1324 + "name": "created_at", 1325 + "type": "integer", 1326 + "primaryKey": false, 1327 + "notNull": false, 1328 + "autoincrement": false, 1329 + "default": "(strftime('%s', 'now'))" 1330 + }, 1331 + "updated_at": { 1332 + "name": "updated_at", 1333 + "type": "integer", 1334 + "primaryKey": false, 1335 + "notNull": false, 1336 + "autoincrement": false, 1337 + "default": "(strftime('%s', 'now'))" 1338 + } 1339 + }, 1340 + "indexes": {}, 1341 + "foreignKeys": { 1342 + "page_subscriber_page_id_page_id_fk": { 1343 + "name": "page_subscriber_page_id_page_id_fk", 1344 + "tableFrom": "page_subscriber", 1345 + "tableTo": "page", 1346 + "columnsFrom": [ 1347 + "page_id" 1348 + ], 1349 + "columnsTo": [ 1350 + "id" 1351 + ], 1352 + "onDelete": "cascade", 1353 + "onUpdate": "no action" 1354 + } 1355 + }, 1356 + "compositePrimaryKeys": {}, 1357 + "uniqueConstraints": {}, 1358 + "checkConstraints": {} 1359 + }, 1360 + "notification": { 1361 + "name": "notification", 1362 + "columns": { 1363 + "id": { 1364 + "name": "id", 1365 + "type": "integer", 1366 + "primaryKey": true, 1367 + "notNull": true, 1368 + "autoincrement": false 1369 + }, 1370 + "name": { 1371 + "name": "name", 1372 + "type": "text", 1373 + "primaryKey": false, 1374 + "notNull": true, 1375 + "autoincrement": false 1376 + }, 1377 + "provider": { 1378 + "name": "provider", 1379 + "type": "text", 1380 + "primaryKey": false, 1381 + "notNull": true, 1382 + "autoincrement": false 1383 + }, 1384 + "data": { 1385 + "name": "data", 1386 + "type": "text", 1387 + "primaryKey": false, 1388 + "notNull": false, 1389 + "autoincrement": false, 1390 + "default": "'{}'" 1391 + }, 1392 + "workspace_id": { 1393 + "name": "workspace_id", 1394 + "type": "integer", 1395 + "primaryKey": false, 1396 + "notNull": false, 1397 + "autoincrement": false 1398 + }, 1399 + "created_at": { 1400 + "name": "created_at", 1401 + "type": "integer", 1402 + "primaryKey": false, 1403 + "notNull": false, 1404 + "autoincrement": false, 1405 + "default": "(strftime('%s', 'now'))" 1406 + }, 1407 + "updated_at": { 1408 + "name": "updated_at", 1409 + "type": "integer", 1410 + "primaryKey": false, 1411 + "notNull": false, 1412 + "autoincrement": false, 1413 + "default": "(strftime('%s', 'now'))" 1414 + } 1415 + }, 1416 + "indexes": {}, 1417 + "foreignKeys": { 1418 + "notification_workspace_id_workspace_id_fk": { 1419 + "name": "notification_workspace_id_workspace_id_fk", 1420 + "tableFrom": "notification", 1421 + "tableTo": "workspace", 1422 + "columnsFrom": [ 1423 + "workspace_id" 1424 + ], 1425 + "columnsTo": [ 1426 + "id" 1427 + ], 1428 + "onDelete": "no action", 1429 + "onUpdate": "no action" 1430 + } 1431 + }, 1432 + "compositePrimaryKeys": {}, 1433 + "uniqueConstraints": {}, 1434 + "checkConstraints": {} 1435 + }, 1436 + "notification_trigger": { 1437 + "name": "notification_trigger", 1438 + "columns": { 1439 + "id": { 1440 + "name": "id", 1441 + "type": "integer", 1442 + "primaryKey": true, 1443 + "notNull": true, 1444 + "autoincrement": false 1445 + }, 1446 + "monitor_id": { 1447 + "name": "monitor_id", 1448 + "type": "integer", 1449 + "primaryKey": false, 1450 + "notNull": false, 1451 + "autoincrement": false 1452 + }, 1453 + "notification_id": { 1454 + "name": "notification_id", 1455 + "type": "integer", 1456 + "primaryKey": false, 1457 + "notNull": false, 1458 + "autoincrement": false 1459 + }, 1460 + "cron_timestamp": { 1461 + "name": "cron_timestamp", 1462 + "type": "integer", 1463 + "primaryKey": false, 1464 + "notNull": true, 1465 + "autoincrement": false 1466 + } 1467 + }, 1468 + "indexes": { 1469 + "notification_id_monitor_id_crontimestampe": { 1470 + "name": "notification_id_monitor_id_crontimestampe", 1471 + "columns": [ 1472 + "notification_id", 1473 + "monitor_id", 1474 + "cron_timestamp" 1475 + ], 1476 + "isUnique": true 1477 + } 1478 + }, 1479 + "foreignKeys": { 1480 + "notification_trigger_monitor_id_monitor_id_fk": { 1481 + "name": "notification_trigger_monitor_id_monitor_id_fk", 1482 + "tableFrom": "notification_trigger", 1483 + "tableTo": "monitor", 1484 + "columnsFrom": [ 1485 + "monitor_id" 1486 + ], 1487 + "columnsTo": [ 1488 + "id" 1489 + ], 1490 + "onDelete": "cascade", 1491 + "onUpdate": "no action" 1492 + }, 1493 + "notification_trigger_notification_id_notification_id_fk": { 1494 + "name": "notification_trigger_notification_id_notification_id_fk", 1495 + "tableFrom": "notification_trigger", 1496 + "tableTo": "notification", 1497 + "columnsFrom": [ 1498 + "notification_id" 1499 + ], 1500 + "columnsTo": [ 1501 + "id" 1502 + ], 1503 + "onDelete": "cascade", 1504 + "onUpdate": "no action" 1505 + } 1506 + }, 1507 + "compositePrimaryKeys": {}, 1508 + "uniqueConstraints": {}, 1509 + "checkConstraints": {} 1510 + }, 1511 + "notifications_to_monitors": { 1512 + "name": "notifications_to_monitors", 1513 + "columns": { 1514 + "monitor_id": { 1515 + "name": "monitor_id", 1516 + "type": "integer", 1517 + "primaryKey": false, 1518 + "notNull": true, 1519 + "autoincrement": false 1520 + }, 1521 + "notification_id": { 1522 + "name": "notification_id", 1523 + "type": "integer", 1524 + "primaryKey": false, 1525 + "notNull": true, 1526 + "autoincrement": false 1527 + }, 1528 + "created_at": { 1529 + "name": "created_at", 1530 + "type": "integer", 1531 + "primaryKey": false, 1532 + "notNull": false, 1533 + "autoincrement": false, 1534 + "default": "(strftime('%s', 'now'))" 1535 + } 1536 + }, 1537 + "indexes": {}, 1538 + "foreignKeys": { 1539 + "notifications_to_monitors_monitor_id_monitor_id_fk": { 1540 + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", 1541 + "tableFrom": "notifications_to_monitors", 1542 + "tableTo": "monitor", 1543 + "columnsFrom": [ 1544 + "monitor_id" 1545 + ], 1546 + "columnsTo": [ 1547 + "id" 1548 + ], 1549 + "onDelete": "cascade", 1550 + "onUpdate": "no action" 1551 + }, 1552 + "notifications_to_monitors_notification_id_notification_id_fk": { 1553 + "name": "notifications_to_monitors_notification_id_notification_id_fk", 1554 + "tableFrom": "notifications_to_monitors", 1555 + "tableTo": "notification", 1556 + "columnsFrom": [ 1557 + "notification_id" 1558 + ], 1559 + "columnsTo": [ 1560 + "id" 1561 + ], 1562 + "onDelete": "cascade", 1563 + "onUpdate": "no action" 1564 + } 1565 + }, 1566 + "compositePrimaryKeys": { 1567 + "notifications_to_monitors_monitor_id_notification_id_pk": { 1568 + "columns": [ 1569 + "monitor_id", 1570 + "notification_id" 1571 + ], 1572 + "name": "notifications_to_monitors_monitor_id_notification_id_pk" 1573 + } 1574 + }, 1575 + "uniqueConstraints": {}, 1576 + "checkConstraints": {} 1577 + }, 1578 + "monitor_status": { 1579 + "name": "monitor_status", 1580 + "columns": { 1581 + "monitor_id": { 1582 + "name": "monitor_id", 1583 + "type": "integer", 1584 + "primaryKey": false, 1585 + "notNull": true, 1586 + "autoincrement": false 1587 + }, 1588 + "region": { 1589 + "name": "region", 1590 + "type": "text", 1591 + "primaryKey": false, 1592 + "notNull": true, 1593 + "autoincrement": false, 1594 + "default": "''" 1595 + }, 1596 + "status": { 1597 + "name": "status", 1598 + "type": "text", 1599 + "primaryKey": false, 1600 + "notNull": true, 1601 + "autoincrement": false, 1602 + "default": "'active'" 1603 + }, 1604 + "created_at": { 1605 + "name": "created_at", 1606 + "type": "integer", 1607 + "primaryKey": false, 1608 + "notNull": false, 1609 + "autoincrement": false, 1610 + "default": "(strftime('%s', 'now'))" 1611 + }, 1612 + "updated_at": { 1613 + "name": "updated_at", 1614 + "type": "integer", 1615 + "primaryKey": false, 1616 + "notNull": false, 1617 + "autoincrement": false, 1618 + "default": "(strftime('%s', 'now'))" 1619 + } 1620 + }, 1621 + "indexes": { 1622 + "monitor_status_idx": { 1623 + "name": "monitor_status_idx", 1624 + "columns": [ 1625 + "monitor_id", 1626 + "region" 1627 + ], 1628 + "isUnique": false 1629 + } 1630 + }, 1631 + "foreignKeys": { 1632 + "monitor_status_monitor_id_monitor_id_fk": { 1633 + "name": "monitor_status_monitor_id_monitor_id_fk", 1634 + "tableFrom": "monitor_status", 1635 + "tableTo": "monitor", 1636 + "columnsFrom": [ 1637 + "monitor_id" 1638 + ], 1639 + "columnsTo": [ 1640 + "id" 1641 + ], 1642 + "onDelete": "cascade", 1643 + "onUpdate": "no action" 1644 + } 1645 + }, 1646 + "compositePrimaryKeys": { 1647 + "monitor_status_monitor_id_region_pk": { 1648 + "columns": [ 1649 + "monitor_id", 1650 + "region" 1651 + ], 1652 + "name": "monitor_status_monitor_id_region_pk" 1653 + } 1654 + }, 1655 + "uniqueConstraints": {}, 1656 + "checkConstraints": {} 1657 + }, 1658 + "invitation": { 1659 + "name": "invitation", 1660 + "columns": { 1661 + "id": { 1662 + "name": "id", 1663 + "type": "integer", 1664 + "primaryKey": true, 1665 + "notNull": true, 1666 + "autoincrement": false 1667 + }, 1668 + "email": { 1669 + "name": "email", 1670 + "type": "text", 1671 + "primaryKey": false, 1672 + "notNull": true, 1673 + "autoincrement": false 1674 + }, 1675 + "role": { 1676 + "name": "role", 1677 + "type": "text", 1678 + "primaryKey": false, 1679 + "notNull": true, 1680 + "autoincrement": false, 1681 + "default": "'member'" 1682 + }, 1683 + "workspace_id": { 1684 + "name": "workspace_id", 1685 + "type": "integer", 1686 + "primaryKey": false, 1687 + "notNull": true, 1688 + "autoincrement": false 1689 + }, 1690 + "token": { 1691 + "name": "token", 1692 + "type": "text", 1693 + "primaryKey": false, 1694 + "notNull": true, 1695 + "autoincrement": false 1696 + }, 1697 + "expires_at": { 1698 + "name": "expires_at", 1699 + "type": "integer", 1700 + "primaryKey": false, 1701 + "notNull": true, 1702 + "autoincrement": false 1703 + }, 1704 + "created_at": { 1705 + "name": "created_at", 1706 + "type": "integer", 1707 + "primaryKey": false, 1708 + "notNull": false, 1709 + "autoincrement": false, 1710 + "default": "(strftime('%s', 'now'))" 1711 + }, 1712 + "accepted_at": { 1713 + "name": "accepted_at", 1714 + "type": "integer", 1715 + "primaryKey": false, 1716 + "notNull": false, 1717 + "autoincrement": false 1718 + } 1719 + }, 1720 + "indexes": {}, 1721 + "foreignKeys": {}, 1722 + "compositePrimaryKeys": {}, 1723 + "uniqueConstraints": {}, 1724 + "checkConstraints": {} 1725 + }, 1726 + "incident": { 1727 + "name": "incident", 1728 + "columns": { 1729 + "id": { 1730 + "name": "id", 1731 + "type": "integer", 1732 + "primaryKey": true, 1733 + "notNull": true, 1734 + "autoincrement": false 1735 + }, 1736 + "title": { 1737 + "name": "title", 1738 + "type": "text", 1739 + "primaryKey": false, 1740 + "notNull": true, 1741 + "autoincrement": false, 1742 + "default": "''" 1743 + }, 1744 + "summary": { 1745 + "name": "summary", 1746 + "type": "text", 1747 + "primaryKey": false, 1748 + "notNull": true, 1749 + "autoincrement": false, 1750 + "default": "''" 1751 + }, 1752 + "status": { 1753 + "name": "status", 1754 + "type": "text", 1755 + "primaryKey": false, 1756 + "notNull": true, 1757 + "autoincrement": false, 1758 + "default": "'triage'" 1759 + }, 1760 + "monitor_id": { 1761 + "name": "monitor_id", 1762 + "type": "integer", 1763 + "primaryKey": false, 1764 + "notNull": false, 1765 + "autoincrement": false 1766 + }, 1767 + "workspace_id": { 1768 + "name": "workspace_id", 1769 + "type": "integer", 1770 + "primaryKey": false, 1771 + "notNull": false, 1772 + "autoincrement": false 1773 + }, 1774 + "started_at": { 1775 + "name": "started_at", 1776 + "type": "integer", 1777 + "primaryKey": false, 1778 + "notNull": true, 1779 + "autoincrement": false, 1780 + "default": "(strftime('%s', 'now'))" 1781 + }, 1782 + "acknowledged_at": { 1783 + "name": "acknowledged_at", 1784 + "type": "integer", 1785 + "primaryKey": false, 1786 + "notNull": false, 1787 + "autoincrement": false 1788 + }, 1789 + "acknowledged_by": { 1790 + "name": "acknowledged_by", 1791 + "type": "integer", 1792 + "primaryKey": false, 1793 + "notNull": false, 1794 + "autoincrement": false 1795 + }, 1796 + "resolved_at": { 1797 + "name": "resolved_at", 1798 + "type": "integer", 1799 + "primaryKey": false, 1800 + "notNull": false, 1801 + "autoincrement": false 1802 + }, 1803 + "resolved_by": { 1804 + "name": "resolved_by", 1805 + "type": "integer", 1806 + "primaryKey": false, 1807 + "notNull": false, 1808 + "autoincrement": false 1809 + }, 1810 + "incident_screenshot_url": { 1811 + "name": "incident_screenshot_url", 1812 + "type": "text", 1813 + "primaryKey": false, 1814 + "notNull": false, 1815 + "autoincrement": false 1816 + }, 1817 + "recovery_screenshot_url": { 1818 + "name": "recovery_screenshot_url", 1819 + "type": "text", 1820 + "primaryKey": false, 1821 + "notNull": false, 1822 + "autoincrement": false 1823 + }, 1824 + "auto_resolved": { 1825 + "name": "auto_resolved", 1826 + "type": "integer", 1827 + "primaryKey": false, 1828 + "notNull": false, 1829 + "autoincrement": false, 1830 + "default": false 1831 + }, 1832 + "created_at": { 1833 + "name": "created_at", 1834 + "type": "integer", 1835 + "primaryKey": false, 1836 + "notNull": false, 1837 + "autoincrement": false, 1838 + "default": "(strftime('%s', 'now'))" 1839 + }, 1840 + "updated_at": { 1841 + "name": "updated_at", 1842 + "type": "integer", 1843 + "primaryKey": false, 1844 + "notNull": false, 1845 + "autoincrement": false, 1846 + "default": "(strftime('%s', 'now'))" 1847 + } 1848 + }, 1849 + "indexes": { 1850 + "incident_monitor_id_started_at_unique": { 1851 + "name": "incident_monitor_id_started_at_unique", 1852 + "columns": [ 1853 + "monitor_id", 1854 + "started_at" 1855 + ], 1856 + "isUnique": true 1857 + } 1858 + }, 1859 + "foreignKeys": { 1860 + "incident_monitor_id_monitor_id_fk": { 1861 + "name": "incident_monitor_id_monitor_id_fk", 1862 + "tableFrom": "incident", 1863 + "tableTo": "monitor", 1864 + "columnsFrom": [ 1865 + "monitor_id" 1866 + ], 1867 + "columnsTo": [ 1868 + "id" 1869 + ], 1870 + "onDelete": "set default", 1871 + "onUpdate": "no action" 1872 + }, 1873 + "incident_workspace_id_workspace_id_fk": { 1874 + "name": "incident_workspace_id_workspace_id_fk", 1875 + "tableFrom": "incident", 1876 + "tableTo": "workspace", 1877 + "columnsFrom": [ 1878 + "workspace_id" 1879 + ], 1880 + "columnsTo": [ 1881 + "id" 1882 + ], 1883 + "onDelete": "no action", 1884 + "onUpdate": "no action" 1885 + }, 1886 + "incident_acknowledged_by_user_id_fk": { 1887 + "name": "incident_acknowledged_by_user_id_fk", 1888 + "tableFrom": "incident", 1889 + "tableTo": "user", 1890 + "columnsFrom": [ 1891 + "acknowledged_by" 1892 + ], 1893 + "columnsTo": [ 1894 + "id" 1895 + ], 1896 + "onDelete": "no action", 1897 + "onUpdate": "no action" 1898 + }, 1899 + "incident_resolved_by_user_id_fk": { 1900 + "name": "incident_resolved_by_user_id_fk", 1901 + "tableFrom": "incident", 1902 + "tableTo": "user", 1903 + "columnsFrom": [ 1904 + "resolved_by" 1905 + ], 1906 + "columnsTo": [ 1907 + "id" 1908 + ], 1909 + "onDelete": "no action", 1910 + "onUpdate": "no action" 1911 + } 1912 + }, 1913 + "compositePrimaryKeys": {}, 1914 + "uniqueConstraints": {}, 1915 + "checkConstraints": {} 1916 + }, 1917 + "monitor_tag": { 1918 + "name": "monitor_tag", 1919 + "columns": { 1920 + "id": { 1921 + "name": "id", 1922 + "type": "integer", 1923 + "primaryKey": true, 1924 + "notNull": true, 1925 + "autoincrement": false 1926 + }, 1927 + "workspace_id": { 1928 + "name": "workspace_id", 1929 + "type": "integer", 1930 + "primaryKey": false, 1931 + "notNull": true, 1932 + "autoincrement": false 1933 + }, 1934 + "name": { 1935 + "name": "name", 1936 + "type": "text", 1937 + "primaryKey": false, 1938 + "notNull": true, 1939 + "autoincrement": false 1940 + }, 1941 + "color": { 1942 + "name": "color", 1943 + "type": "text", 1944 + "primaryKey": false, 1945 + "notNull": true, 1946 + "autoincrement": false 1947 + }, 1948 + "created_at": { 1949 + "name": "created_at", 1950 + "type": "integer", 1951 + "primaryKey": false, 1952 + "notNull": false, 1953 + "autoincrement": false, 1954 + "default": "(strftime('%s', 'now'))" 1955 + }, 1956 + "updated_at": { 1957 + "name": "updated_at", 1958 + "type": "integer", 1959 + "primaryKey": false, 1960 + "notNull": false, 1961 + "autoincrement": false, 1962 + "default": "(strftime('%s', 'now'))" 1963 + } 1964 + }, 1965 + "indexes": {}, 1966 + "foreignKeys": { 1967 + "monitor_tag_workspace_id_workspace_id_fk": { 1968 + "name": "monitor_tag_workspace_id_workspace_id_fk", 1969 + "tableFrom": "monitor_tag", 1970 + "tableTo": "workspace", 1971 + "columnsFrom": [ 1972 + "workspace_id" 1973 + ], 1974 + "columnsTo": [ 1975 + "id" 1976 + ], 1977 + "onDelete": "cascade", 1978 + "onUpdate": "no action" 1979 + } 1980 + }, 1981 + "compositePrimaryKeys": {}, 1982 + "uniqueConstraints": {}, 1983 + "checkConstraints": {} 1984 + }, 1985 + "monitor_tag_to_monitor": { 1986 + "name": "monitor_tag_to_monitor", 1987 + "columns": { 1988 + "monitor_id": { 1989 + "name": "monitor_id", 1990 + "type": "integer", 1991 + "primaryKey": false, 1992 + "notNull": true, 1993 + "autoincrement": false 1994 + }, 1995 + "monitor_tag_id": { 1996 + "name": "monitor_tag_id", 1997 + "type": "integer", 1998 + "primaryKey": false, 1999 + "notNull": true, 2000 + "autoincrement": false 2001 + }, 2002 + "created_at": { 2003 + "name": "created_at", 2004 + "type": "integer", 2005 + "primaryKey": false, 2006 + "notNull": false, 2007 + "autoincrement": false, 2008 + "default": "(strftime('%s', 'now'))" 2009 + } 2010 + }, 2011 + "indexes": {}, 2012 + "foreignKeys": { 2013 + "monitor_tag_to_monitor_monitor_id_monitor_id_fk": { 2014 + "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk", 2015 + "tableFrom": "monitor_tag_to_monitor", 2016 + "tableTo": "monitor", 2017 + "columnsFrom": [ 2018 + "monitor_id" 2019 + ], 2020 + "columnsTo": [ 2021 + "id" 2022 + ], 2023 + "onDelete": "cascade", 2024 + "onUpdate": "no action" 2025 + }, 2026 + "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": { 2027 + "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk", 2028 + "tableFrom": "monitor_tag_to_monitor", 2029 + "tableTo": "monitor_tag", 2030 + "columnsFrom": [ 2031 + "monitor_tag_id" 2032 + ], 2033 + "columnsTo": [ 2034 + "id" 2035 + ], 2036 + "onDelete": "cascade", 2037 + "onUpdate": "no action" 2038 + } 2039 + }, 2040 + "compositePrimaryKeys": { 2041 + "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": { 2042 + "columns": [ 2043 + "monitor_id", 2044 + "monitor_tag_id" 2045 + ], 2046 + "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk" 2047 + } 2048 + }, 2049 + "uniqueConstraints": {}, 2050 + "checkConstraints": {} 2051 + }, 2052 + "application": { 2053 + "name": "application", 2054 + "columns": { 2055 + "id": { 2056 + "name": "id", 2057 + "type": "integer", 2058 + "primaryKey": true, 2059 + "notNull": true, 2060 + "autoincrement": false 2061 + }, 2062 + "name": { 2063 + "name": "name", 2064 + "type": "text", 2065 + "primaryKey": false, 2066 + "notNull": false, 2067 + "autoincrement": false 2068 + }, 2069 + "dsn": { 2070 + "name": "dsn", 2071 + "type": "text", 2072 + "primaryKey": false, 2073 + "notNull": false, 2074 + "autoincrement": false 2075 + }, 2076 + "workspace_id": { 2077 + "name": "workspace_id", 2078 + "type": "integer", 2079 + "primaryKey": false, 2080 + "notNull": false, 2081 + "autoincrement": false 2082 + }, 2083 + "created_at": { 2084 + "name": "created_at", 2085 + "type": "integer", 2086 + "primaryKey": false, 2087 + "notNull": false, 2088 + "autoincrement": false, 2089 + "default": "(strftime('%s', 'now'))" 2090 + }, 2091 + "updated_at": { 2092 + "name": "updated_at", 2093 + "type": "integer", 2094 + "primaryKey": false, 2095 + "notNull": false, 2096 + "autoincrement": false, 2097 + "default": "(strftime('%s', 'now'))" 2098 + } 2099 + }, 2100 + "indexes": { 2101 + "application_dsn_unique": { 2102 + "name": "application_dsn_unique", 2103 + "columns": [ 2104 + "dsn" 2105 + ], 2106 + "isUnique": true 2107 + } 2108 + }, 2109 + "foreignKeys": { 2110 + "application_workspace_id_workspace_id_fk": { 2111 + "name": "application_workspace_id_workspace_id_fk", 2112 + "tableFrom": "application", 2113 + "tableTo": "workspace", 2114 + "columnsFrom": [ 2115 + "workspace_id" 2116 + ], 2117 + "columnsTo": [ 2118 + "id" 2119 + ], 2120 + "onDelete": "no action", 2121 + "onUpdate": "no action" 2122 + } 2123 + }, 2124 + "compositePrimaryKeys": {}, 2125 + "uniqueConstraints": {}, 2126 + "checkConstraints": {} 2127 + }, 2128 + "maintenance": { 2129 + "name": "maintenance", 2130 + "columns": { 2131 + "id": { 2132 + "name": "id", 2133 + "type": "integer", 2134 + "primaryKey": true, 2135 + "notNull": true, 2136 + "autoincrement": false 2137 + }, 2138 + "title": { 2139 + "name": "title", 2140 + "type": "text(256)", 2141 + "primaryKey": false, 2142 + "notNull": true, 2143 + "autoincrement": false 2144 + }, 2145 + "message": { 2146 + "name": "message", 2147 + "type": "text", 2148 + "primaryKey": false, 2149 + "notNull": true, 2150 + "autoincrement": false 2151 + }, 2152 + "from": { 2153 + "name": "from", 2154 + "type": "integer", 2155 + "primaryKey": false, 2156 + "notNull": true, 2157 + "autoincrement": false 2158 + }, 2159 + "to": { 2160 + "name": "to", 2161 + "type": "integer", 2162 + "primaryKey": false, 2163 + "notNull": true, 2164 + "autoincrement": false 2165 + }, 2166 + "workspace_id": { 2167 + "name": "workspace_id", 2168 + "type": "integer", 2169 + "primaryKey": false, 2170 + "notNull": false, 2171 + "autoincrement": false 2172 + }, 2173 + "page_id": { 2174 + "name": "page_id", 2175 + "type": "integer", 2176 + "primaryKey": false, 2177 + "notNull": false, 2178 + "autoincrement": false 2179 + }, 2180 + "created_at": { 2181 + "name": "created_at", 2182 + "type": "integer", 2183 + "primaryKey": false, 2184 + "notNull": false, 2185 + "autoincrement": false, 2186 + "default": "(strftime('%s', 'now'))" 2187 + }, 2188 + "updated_at": { 2189 + "name": "updated_at", 2190 + "type": "integer", 2191 + "primaryKey": false, 2192 + "notNull": false, 2193 + "autoincrement": false, 2194 + "default": "(strftime('%s', 'now'))" 2195 + } 2196 + }, 2197 + "indexes": {}, 2198 + "foreignKeys": { 2199 + "maintenance_workspace_id_workspace_id_fk": { 2200 + "name": "maintenance_workspace_id_workspace_id_fk", 2201 + "tableFrom": "maintenance", 2202 + "tableTo": "workspace", 2203 + "columnsFrom": [ 2204 + "workspace_id" 2205 + ], 2206 + "columnsTo": [ 2207 + "id" 2208 + ], 2209 + "onDelete": "no action", 2210 + "onUpdate": "no action" 2211 + }, 2212 + "maintenance_page_id_page_id_fk": { 2213 + "name": "maintenance_page_id_page_id_fk", 2214 + "tableFrom": "maintenance", 2215 + "tableTo": "page", 2216 + "columnsFrom": [ 2217 + "page_id" 2218 + ], 2219 + "columnsTo": [ 2220 + "id" 2221 + ], 2222 + "onDelete": "cascade", 2223 + "onUpdate": "no action" 2224 + } 2225 + }, 2226 + "compositePrimaryKeys": {}, 2227 + "uniqueConstraints": {}, 2228 + "checkConstraints": {} 2229 + }, 2230 + "maintenance_to_monitor": { 2231 + "name": "maintenance_to_monitor", 2232 + "columns": { 2233 + "maintenance_id": { 2234 + "name": "maintenance_id", 2235 + "type": "integer", 2236 + "primaryKey": false, 2237 + "notNull": true, 2238 + "autoincrement": false 2239 + }, 2240 + "monitor_id": { 2241 + "name": "monitor_id", 2242 + "type": "integer", 2243 + "primaryKey": false, 2244 + "notNull": true, 2245 + "autoincrement": false 2246 + }, 2247 + "created_at": { 2248 + "name": "created_at", 2249 + "type": "integer", 2250 + "primaryKey": false, 2251 + "notNull": false, 2252 + "autoincrement": false, 2253 + "default": "(strftime('%s', 'now'))" 2254 + } 2255 + }, 2256 + "indexes": {}, 2257 + "foreignKeys": { 2258 + "maintenance_to_monitor_maintenance_id_maintenance_id_fk": { 2259 + "name": "maintenance_to_monitor_maintenance_id_maintenance_id_fk", 2260 + "tableFrom": "maintenance_to_monitor", 2261 + "tableTo": "maintenance", 2262 + "columnsFrom": [ 2263 + "maintenance_id" 2264 + ], 2265 + "columnsTo": [ 2266 + "id" 2267 + ], 2268 + "onDelete": "cascade", 2269 + "onUpdate": "no action" 2270 + }, 2271 + "maintenance_to_monitor_monitor_id_monitor_id_fk": { 2272 + "name": "maintenance_to_monitor_monitor_id_monitor_id_fk", 2273 + "tableFrom": "maintenance_to_monitor", 2274 + "tableTo": "monitor", 2275 + "columnsFrom": [ 2276 + "monitor_id" 2277 + ], 2278 + "columnsTo": [ 2279 + "id" 2280 + ], 2281 + "onDelete": "cascade", 2282 + "onUpdate": "no action" 2283 + } 2284 + }, 2285 + "compositePrimaryKeys": { 2286 + "maintenance_to_monitor_maintenance_id_monitor_id_pk": { 2287 + "columns": [ 2288 + "maintenance_id", 2289 + "monitor_id" 2290 + ], 2291 + "name": "maintenance_to_monitor_maintenance_id_monitor_id_pk" 2292 + } 2293 + }, 2294 + "uniqueConstraints": {}, 2295 + "checkConstraints": {} 2296 + }, 2297 + "check": { 2298 + "name": "check", 2299 + "columns": { 2300 + "id": { 2301 + "name": "id", 2302 + "type": "integer", 2303 + "primaryKey": true, 2304 + "notNull": true, 2305 + "autoincrement": true 2306 + }, 2307 + "regions": { 2308 + "name": "regions", 2309 + "type": "text", 2310 + "primaryKey": false, 2311 + "notNull": true, 2312 + "autoincrement": false, 2313 + "default": "''" 2314 + }, 2315 + "url": { 2316 + "name": "url", 2317 + "type": "text(4096)", 2318 + "primaryKey": false, 2319 + "notNull": true, 2320 + "autoincrement": false 2321 + }, 2322 + "headers": { 2323 + "name": "headers", 2324 + "type": "text", 2325 + "primaryKey": false, 2326 + "notNull": false, 2327 + "autoincrement": false, 2328 + "default": "''" 2329 + }, 2330 + "body": { 2331 + "name": "body", 2332 + "type": "text", 2333 + "primaryKey": false, 2334 + "notNull": false, 2335 + "autoincrement": false, 2336 + "default": "''" 2337 + }, 2338 + "method": { 2339 + "name": "method", 2340 + "type": "text", 2341 + "primaryKey": false, 2342 + "notNull": false, 2343 + "autoincrement": false, 2344 + "default": "'GET'" 2345 + }, 2346 + "count_requests": { 2347 + "name": "count_requests", 2348 + "type": "integer", 2349 + "primaryKey": false, 2350 + "notNull": false, 2351 + "autoincrement": false, 2352 + "default": 1 2353 + }, 2354 + "workspace_id": { 2355 + "name": "workspace_id", 2356 + "type": "integer", 2357 + "primaryKey": false, 2358 + "notNull": false, 2359 + "autoincrement": false 2360 + }, 2361 + "created_at": { 2362 + "name": "created_at", 2363 + "type": "integer", 2364 + "primaryKey": false, 2365 + "notNull": false, 2366 + "autoincrement": false, 2367 + "default": "(strftime('%s', 'now'))" 2368 + } 2369 + }, 2370 + "indexes": {}, 2371 + "foreignKeys": { 2372 + "check_workspace_id_workspace_id_fk": { 2373 + "name": "check_workspace_id_workspace_id_fk", 2374 + "tableFrom": "check", 2375 + "tableTo": "workspace", 2376 + "columnsFrom": [ 2377 + "workspace_id" 2378 + ], 2379 + "columnsTo": [ 2380 + "id" 2381 + ], 2382 + "onDelete": "no action", 2383 + "onUpdate": "no action" 2384 + } 2385 + }, 2386 + "compositePrimaryKeys": {}, 2387 + "uniqueConstraints": {}, 2388 + "checkConstraints": {} 2389 + }, 2390 + "monitor_run": { 2391 + "name": "monitor_run", 2392 + "columns": { 2393 + "id": { 2394 + "name": "id", 2395 + "type": "integer", 2396 + "primaryKey": true, 2397 + "notNull": true, 2398 + "autoincrement": false 2399 + }, 2400 + "workspace_id": { 2401 + "name": "workspace_id", 2402 + "type": "integer", 2403 + "primaryKey": false, 2404 + "notNull": false, 2405 + "autoincrement": false 2406 + }, 2407 + "monitor_id": { 2408 + "name": "monitor_id", 2409 + "type": "integer", 2410 + "primaryKey": false, 2411 + "notNull": false, 2412 + "autoincrement": false 2413 + }, 2414 + "runned_at": { 2415 + "name": "runned_at", 2416 + "type": "integer", 2417 + "primaryKey": false, 2418 + "notNull": false, 2419 + "autoincrement": false 2420 + }, 2421 + "created_at": { 2422 + "name": "created_at", 2423 + "type": "integer", 2424 + "primaryKey": false, 2425 + "notNull": false, 2426 + "autoincrement": false, 2427 + "default": "(strftime('%s', 'now'))" 2428 + } 2429 + }, 2430 + "indexes": {}, 2431 + "foreignKeys": { 2432 + "monitor_run_workspace_id_workspace_id_fk": { 2433 + "name": "monitor_run_workspace_id_workspace_id_fk", 2434 + "tableFrom": "monitor_run", 2435 + "tableTo": "workspace", 2436 + "columnsFrom": [ 2437 + "workspace_id" 2438 + ], 2439 + "columnsTo": [ 2440 + "id" 2441 + ], 2442 + "onDelete": "no action", 2443 + "onUpdate": "no action" 2444 + }, 2445 + "monitor_run_monitor_id_monitor_id_fk": { 2446 + "name": "monitor_run_monitor_id_monitor_id_fk", 2447 + "tableFrom": "monitor_run", 2448 + "tableTo": "monitor", 2449 + "columnsFrom": [ 2450 + "monitor_id" 2451 + ], 2452 + "columnsTo": [ 2453 + "id" 2454 + ], 2455 + "onDelete": "no action", 2456 + "onUpdate": "no action" 2457 + } 2458 + }, 2459 + "compositePrimaryKeys": {}, 2460 + "uniqueConstraints": {}, 2461 + "checkConstraints": {} 2462 + }, 2463 + "private_location": { 2464 + "name": "private_location", 2465 + "columns": { 2466 + "id": { 2467 + "name": "id", 2468 + "type": "integer", 2469 + "primaryKey": true, 2470 + "notNull": true, 2471 + "autoincrement": false 2472 + }, 2473 + "name": { 2474 + "name": "name", 2475 + "type": "text", 2476 + "primaryKey": false, 2477 + "notNull": true, 2478 + "autoincrement": false 2479 + }, 2480 + "token": { 2481 + "name": "token", 2482 + "type": "text", 2483 + "primaryKey": false, 2484 + "notNull": true, 2485 + "autoincrement": false 2486 + }, 2487 + "last_seen_at": { 2488 + "name": "last_seen_at", 2489 + "type": "integer", 2490 + "primaryKey": false, 2491 + "notNull": false, 2492 + "autoincrement": false 2493 + }, 2494 + "workspace_id": { 2495 + "name": "workspace_id", 2496 + "type": "integer", 2497 + "primaryKey": false, 2498 + "notNull": false, 2499 + "autoincrement": false 2500 + }, 2501 + "created_at": { 2502 + "name": "created_at", 2503 + "type": "integer", 2504 + "primaryKey": false, 2505 + "notNull": false, 2506 + "autoincrement": false, 2507 + "default": "(strftime('%s', 'now'))" 2508 + }, 2509 + "updated_at": { 2510 + "name": "updated_at", 2511 + "type": "integer", 2512 + "primaryKey": false, 2513 + "notNull": false, 2514 + "autoincrement": false, 2515 + "default": "(strftime('%s', 'now'))" 2516 + } 2517 + }, 2518 + "indexes": {}, 2519 + "foreignKeys": { 2520 + "private_location_workspace_id_workspace_id_fk": { 2521 + "name": "private_location_workspace_id_workspace_id_fk", 2522 + "tableFrom": "private_location", 2523 + "tableTo": "workspace", 2524 + "columnsFrom": [ 2525 + "workspace_id" 2526 + ], 2527 + "columnsTo": [ 2528 + "id" 2529 + ], 2530 + "onDelete": "no action", 2531 + "onUpdate": "no action" 2532 + } 2533 + }, 2534 + "compositePrimaryKeys": {}, 2535 + "uniqueConstraints": {}, 2536 + "checkConstraints": {} 2537 + }, 2538 + "private_location_to_monitor": { 2539 + "name": "private_location_to_monitor", 2540 + "columns": { 2541 + "private_location_id": { 2542 + "name": "private_location_id", 2543 + "type": "integer", 2544 + "primaryKey": false, 2545 + "notNull": false, 2546 + "autoincrement": false 2547 + }, 2548 + "monitor_id": { 2549 + "name": "monitor_id", 2550 + "type": "integer", 2551 + "primaryKey": false, 2552 + "notNull": false, 2553 + "autoincrement": false 2554 + }, 2555 + "created_at": { 2556 + "name": "created_at", 2557 + "type": "integer", 2558 + "primaryKey": false, 2559 + "notNull": false, 2560 + "autoincrement": false, 2561 + "default": "(strftime('%s', 'now'))" 2562 + }, 2563 + "deleted_at": { 2564 + "name": "deleted_at", 2565 + "type": "integer", 2566 + "primaryKey": false, 2567 + "notNull": false, 2568 + "autoincrement": false 2569 + } 2570 + }, 2571 + "indexes": {}, 2572 + "foreignKeys": { 2573 + "private_location_to_monitor_private_location_id_private_location_id_fk": { 2574 + "name": "private_location_to_monitor_private_location_id_private_location_id_fk", 2575 + "tableFrom": "private_location_to_monitor", 2576 + "tableTo": "private_location", 2577 + "columnsFrom": [ 2578 + "private_location_id" 2579 + ], 2580 + "columnsTo": [ 2581 + "id" 2582 + ], 2583 + "onDelete": "cascade", 2584 + "onUpdate": "no action" 2585 + }, 2586 + "private_location_to_monitor_monitor_id_monitor_id_fk": { 2587 + "name": "private_location_to_monitor_monitor_id_monitor_id_fk", 2588 + "tableFrom": "private_location_to_monitor", 2589 + "tableTo": "monitor", 2590 + "columnsFrom": [ 2591 + "monitor_id" 2592 + ], 2593 + "columnsTo": [ 2594 + "id" 2595 + ], 2596 + "onDelete": "cascade", 2597 + "onUpdate": "no action" 2598 + } 2599 + }, 2600 + "compositePrimaryKeys": {}, 2601 + "uniqueConstraints": {}, 2602 + "checkConstraints": {} 2603 + }, 2604 + "monitor_group": { 2605 + "name": "monitor_group", 2606 + "columns": { 2607 + "id": { 2608 + "name": "id", 2609 + "type": "integer", 2610 + "primaryKey": true, 2611 + "notNull": true, 2612 + "autoincrement": false 2613 + }, 2614 + "workspace_id": { 2615 + "name": "workspace_id", 2616 + "type": "integer", 2617 + "primaryKey": false, 2618 + "notNull": true, 2619 + "autoincrement": false 2620 + }, 2621 + "page_id": { 2622 + "name": "page_id", 2623 + "type": "integer", 2624 + "primaryKey": false, 2625 + "notNull": true, 2626 + "autoincrement": false 2627 + }, 2628 + "name": { 2629 + "name": "name", 2630 + "type": "text", 2631 + "primaryKey": false, 2632 + "notNull": true, 2633 + "autoincrement": false 2634 + }, 2635 + "created_at": { 2636 + "name": "created_at", 2637 + "type": "integer", 2638 + "primaryKey": false, 2639 + "notNull": false, 2640 + "autoincrement": false, 2641 + "default": "(strftime('%s', 'now'))" 2642 + }, 2643 + "updated_at": { 2644 + "name": "updated_at", 2645 + "type": "integer", 2646 + "primaryKey": false, 2647 + "notNull": false, 2648 + "autoincrement": false, 2649 + "default": "(strftime('%s', 'now'))" 2650 + } 2651 + }, 2652 + "indexes": {}, 2653 + "foreignKeys": { 2654 + "monitor_group_workspace_id_workspace_id_fk": { 2655 + "name": "monitor_group_workspace_id_workspace_id_fk", 2656 + "tableFrom": "monitor_group", 2657 + "tableTo": "workspace", 2658 + "columnsFrom": [ 2659 + "workspace_id" 2660 + ], 2661 + "columnsTo": [ 2662 + "id" 2663 + ], 2664 + "onDelete": "cascade", 2665 + "onUpdate": "no action" 2666 + }, 2667 + "monitor_group_page_id_page_id_fk": { 2668 + "name": "monitor_group_page_id_page_id_fk", 2669 + "tableFrom": "monitor_group", 2670 + "tableTo": "page", 2671 + "columnsFrom": [ 2672 + "page_id" 2673 + ], 2674 + "columnsTo": [ 2675 + "id" 2676 + ], 2677 + "onDelete": "cascade", 2678 + "onUpdate": "no action" 2679 + } 2680 + }, 2681 + "compositePrimaryKeys": {}, 2682 + "uniqueConstraints": {}, 2683 + "checkConstraints": {} 2684 + } 2685 + }, 2686 + "views": {}, 2687 + "enums": {}, 2688 + "_meta": { 2689 + "schemas": {}, 2690 + "tables": {}, 2691 + "columns": {} 2692 + }, 2693 + "internal": { 2694 + "indexes": {} 2695 + } 2696 + }
+7
packages/db/drizzle/meta/_journal.json
··· 344 344 "when": 1760602903085, 345 345 "tag": "0048_neat_tempest", 346 346 "breakpoints": true 347 + }, 348 + { 349 + "idx": 49, 350 + "version": "6", 351 + "when": 1761901661043, 352 + "tag": "0049_sloppy_inhumans", 353 + "breakpoints": true 347 354 } 348 355 ] 349 356 }
+1
packages/db/src/schema/index.ts
··· 16 16 export * from "./check"; 17 17 export * from "./monitor_run"; 18 18 export * from "./private_locations"; 19 + export * from "./monitor_groups";
+2
packages/db/src/schema/monitor_groups/index.ts
··· 1 + export * from "./monitor_group"; 2 + export * from "./validation";
+23
packages/db/src/schema/monitor_groups/monitor_group.ts
··· 1 + import { sql } from "drizzle-orm"; 2 + import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 3 + 4 + import { page } from "../pages"; 5 + import { workspace } from "../workspaces"; 6 + 7 + export const monitorGroup = sqliteTable("monitor_group", { 8 + id: integer("id").primaryKey(), 9 + workspaceId: integer("workspace_id") 10 + .references(() => workspace.id, { onDelete: "cascade" }) 11 + .notNull(), 12 + pageId: integer("page_id") 13 + .references(() => page.id, { onDelete: "cascade" }) 14 + .notNull(), 15 + name: text("name").notNull(), 16 + 17 + createdAt: integer("created_at", { mode: "timestamp" }).default( 18 + sql`(strftime('%s', 'now'))`, 19 + ), 20 + updatedAt: integer("updated_at", { mode: "timestamp" }).default( 21 + sql`(strftime('%s', 'now'))`, 22 + ), 23 + });
+11
packages/db/src/schema/monitor_groups/validation.ts
··· 1 + import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 2 + import type { z } from "zod"; 3 + 4 + import { monitorGroup } from "./monitor_group"; 5 + 6 + export const selectMonitorGroupSchema = createSelectSchema(monitorGroup); 7 + 8 + export const insertMonitorGroupSchema = createInsertSchema(monitorGroup); 9 + 10 + export type InsertMonitorGroup = z.infer<typeof insertMonitorGroupSchema>; 11 + export type MonitorGroup = z.infer<typeof selectMonitorGroupSchema>;
+11
packages/db/src/schema/monitors/monitor.ts
··· 9 9 import { monitorPeriodicity } from "../constants"; 10 10 import { incidentTable } from "../incidents/incident"; 11 11 import { maintenancesToMonitors } from "../maintenances"; 12 + import { monitorGroup } from "../monitor_groups"; 12 13 import { monitorStatusTable } from "../monitor_status/monitor_status"; 13 14 import { monitorTagsToMonitors } from "../monitor_tags"; 14 15 import { notificationsToMonitors } from "../notifications"; ··· 99 100 sql`(strftime('%s', 'now'))`, 100 101 ), 101 102 order: integer("order").default(0), 103 + 104 + monitorGroupId: integer("monitor_group_id").references( 105 + () => monitorGroup.id, 106 + { onDelete: "cascade" }, 107 + ), 108 + groupOrder: integer("group_order").default(0), 102 109 }, 103 110 (t) => ({ 104 111 pk: primaryKey({ columns: [t.monitorId, t.pageId] }), ··· 115 122 page: one(page, { 116 123 fields: [monitorsToPages.pageId], 117 124 references: [page.id], 125 + }), 126 + monitorGroup: one(monitorGroup, { 127 + fields: [monitorsToPages.monitorGroupId], 128 + references: [monitorGroup.id], 118 129 }), 119 130 }), 120 131 );
+33 -7
packages/db/src/schema/shared.ts
··· 2 2 3 3 import { selectIncidentSchema } from "./incidents/validation"; 4 4 import { selectMaintenanceSchema } from "./maintenances"; 5 + import { selectMonitorGroupSchema } from "./monitor_groups"; 5 6 import { selectMonitorSchema } from "./monitors"; 6 7 import { selectPageSchema } from "./pages"; 7 8 import { ··· 84 85 id: true, 85 86 }); 86 87 88 + const selectPublicMonitorWithStatusSchema = selectPublicMonitorSchema.extend({ 89 + status: z.enum(["success", "degraded", "error", "info"]).default("success"), 90 + monitorGroupId: z.number().nullable().optional(), 91 + order: z.number().default(0).optional(), 92 + groupOrder: z.number().default(0).optional(), 93 + }); 94 + 95 + const trackersSchema = z 96 + .array( 97 + z.discriminatedUnion("type", [ 98 + z.object({ 99 + type: z.literal("monitor"), 100 + monitor: selectPublicMonitorWithStatusSchema, 101 + order: z.number(), 102 + }), 103 + z.object({ 104 + type: z.literal("group"), 105 + groupId: z.number(), 106 + groupName: z.string(), 107 + monitors: z.array(selectPublicMonitorWithStatusSchema), 108 + status: z 109 + .enum(["success", "degraded", "error", "info"]) 110 + .default("success"), 111 + order: z.number(), 112 + }), 113 + ]), 114 + ) 115 + .default([]); 116 + 87 117 export const selectPublicPageSchemaWithRelation = selectPageSchema 88 118 .extend({ 119 + monitorGroups: selectMonitorGroupSchema.array().default([]), 89 120 // TODO: include status of the monitor 90 - monitors: selectPublicMonitorSchema 91 - .extend({ 92 - status: z 93 - .enum(["success", "degraded", "error", "info"]) 94 - .default("success"), 95 - }) 96 - .array(), 121 + monitors: selectPublicMonitorWithStatusSchema.array(), 122 + trackers: trackersSchema, 97 123 lastEvents: z.array( 98 124 z.object({ 99 125 id: z.number(),