) {
+ return (
+
+ )
+}
+
+function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+ a]:underline tw:[&>a]:underline-offset-4 tw:[&>a:hover]:text-primary",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Empty,
+ EmptyHeader,
+ EmptyTitle,
+ EmptyDescription,
+ EmptyContent,
+ EmptyMedia,
+}
diff --git a/packages/admin-ui/src/components/primitives/field.tsx b/packages/admin-ui/src/components/primitives/field.tsx
new file mode 100644
index 0000000000..133266af63
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/field.tsx
@@ -0,0 +1,237 @@
+import * as React from "react"
+import { useMemo } from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "lib/utils"
+import { Label } from "components/primitives/label"
+import { Separator } from "components/primitives/separator"
+
+function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
+ return (
+
[data-slot=checkbox-group]]:gap-3 tw:has-[>[data-slot=radio-group]]:gap-3",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldLegend({
+ className,
+ variant = "legend",
+ ...props
+}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
+ return (
+
+ )
+}
+
+function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+const fieldVariants = cva(
+ "tw:group/field tw:flex tw:w-full tw:gap-2 tw:data-[invalid=true]:text-destructive",
+ {
+ variants: {
+ orientation: {
+ vertical: "tw:flex-col tw:*:w-full tw:[&>.sr-only]:w-auto",
+ horizontal:
+ "tw:flex-row tw:items-center tw:has-[>[data-slot=field-content]]:items-start tw:*:data-[slot=field-label]:flex-auto tw:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
+ responsive:
+ "tw:flex-col tw:*:w-full tw:@md/field-group:flex-row tw:@md/field-group:items-center tw:@md/field-group:*:w-auto tw:@md/field-group:has-[>[data-slot=field-content]]:items-start tw:@md/field-group:*:data-[slot=field-label]:flex-auto tw:[&>.sr-only]:w-auto tw:@md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
+ },
+ },
+ defaultVariants: {
+ orientation: "vertical",
+ },
+ }
+)
+
+function Field({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function FieldLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+ [data-slot=field]]:rounded-lg tw:has-[>[data-slot=field]]:border tw:*:data-[slot=field]:p-2.5 tw:dark:has-data-[state=checked]:border-primary/20 tw:dark:has-data-[state=checked]:bg-primary/10",
+ "tw:has-[>[data-slot=field]]:w-full tw:has-[>[data-slot=field]]:flex-col",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+ a]:underline tw:[&>a]:underline-offset-4 tw:[&>a:hover]:text-primary",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function FieldSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"div"> & {
+ children?: React.ReactNode
+}) {
+ return (
+
+
+ {children && (
+
+ {children}
+
+ )}
+
+ )
+}
+
+function FieldError({
+ className,
+ children,
+ errors,
+ ...props
+}: React.ComponentProps<"div"> & {
+ errors?: Array<{ message?: string } | undefined>
+}) {
+ const content = useMemo(() => {
+ if (children) {
+ return children
+ }
+
+ if (!errors?.length) {
+ return null
+ }
+
+ const uniqueErrors = [
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
+ ]
+
+ if (uniqueErrors?.length == 1) {
+ return uniqueErrors[0]?.message
+ }
+
+ return (
+
+ {uniqueErrors.map(
+ (error, index) =>
+ error?.message && {error.message}
+ )}
+
+ )
+ }, [children, errors])
+
+ if (!content) {
+ return null
+ }
+
+ return (
+
+ {content}
+
+ )
+}
+
+export {
+ Field,
+ FieldLabel,
+ FieldDescription,
+ FieldError,
+ FieldGroup,
+ FieldLegend,
+ FieldSeparator,
+ FieldSet,
+ FieldContent,
+ FieldTitle,
+}
diff --git a/packages/admin-ui/src/components/primitives/hover-card.tsx b/packages/admin-ui/src/components/primitives/hover-card.tsx
new file mode 100644
index 0000000000..70892edc53
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/hover-card.tsx
@@ -0,0 +1,36 @@
+import * as React from "react";
+import { HoverCard as HoverCardPrimitive } from "radix-ui";
+
+import { cn } from "lib/utils";
+
+function HoverCard({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function HoverCardTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function HoverCardContent({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+export { HoverCard, HoverCardTrigger, HoverCardContent };
diff --git a/packages/admin-ui/src/components/primitives/input.tsx b/packages/admin-ui/src/components/primitives/input.tsx
new file mode 100644
index 0000000000..f61dc5de0a
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/input.tsx
@@ -0,0 +1,19 @@
+import * as React from "react";
+
+import { cn } from "lib/utils";
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ );
+}
+
+export { Input };
diff --git a/packages/admin-ui/src/components/primitives/item.tsx b/packages/admin-ui/src/components/primitives/item.tsx
new file mode 100644
index 0000000000..bc6fd1c3fe
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/item.tsx
@@ -0,0 +1,196 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Slot } from "radix-ui"
+
+import { cn } from "lib/utils"
+import { Separator } from "components/primitives/separator"
+
+function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const itemVariants = cva(
+ "tw:group/item tw:flex tw:w-full tw:flex-wrap tw:items-center tw:rounded-lg tw:border tw:text-sm tw:transition-colors tw:duration-100 tw:outline-none tw:focus-visible:border-ring tw:focus-visible:ring-[3px] tw:focus-visible:ring-ring/50 tw:[a]:transition-colors tw:[a]:hover:bg-muted",
+ {
+ variants: {
+ variant: {
+ default: "tw:border-transparent",
+ outline: "tw:border-border",
+ muted: "tw:border-transparent tw:bg-muted/50",
+ },
+ size: {
+ default: "tw:gap-2.5 tw:px-3 tw:py-2.5",
+ sm: "tw:gap-2.5 tw:px-3 tw:py-2.5",
+ xs: "tw:gap-2 tw:px-2.5 tw:py-2 tw:in-data-[slot=dropdown-menu-content]:p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Item({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = (asChild ? Slot.Root : "div") as React.ElementType
+ return (
+
+ )
+}
+
+const itemMediaVariants = cva(
+ "tw:flex tw:shrink-0 tw:items-center tw:justify-center tw:gap-2 tw:group-has-data-[slot=item-description]/item:translate-y-0.5 tw:group-has-data-[slot=item-description]/item:self-start tw:[&_svg]:pointer-events-none",
+ {
+ variants: {
+ variant: {
+ default: "tw:bg-transparent",
+ icon: "tw:[&_svg:not([class*=size-])]:size-4",
+ image:
+ "tw:size-10 tw:overflow-hidden tw:rounded-sm tw:group-data-[size=sm]/item:size-8 tw:group-data-[size=xs]/item:size-6 tw:[&_img]:size-full tw:[&_img]:object-cover",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function ItemMedia({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+ a]:underline tw:[&>a]:underline-offset-4 tw:[&>a:hover]:text-primary",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Item,
+ ItemMedia,
+ ItemContent,
+ ItemActions,
+ ItemGroup,
+ ItemSeparator,
+ ItemTitle,
+ ItemDescription,
+ ItemHeader,
+ ItemFooter,
+}
diff --git a/packages/admin-ui/src/components/primitives/label.tsx b/packages/admin-ui/src/components/primitives/label.tsx
new file mode 100644
index 0000000000..7f164ff0fe
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/label.tsx
@@ -0,0 +1,19 @@
+import * as React from "react";
+import { Label as LabelPrimitive } from "radix-ui";
+
+import { cn } from "lib/utils";
+
+function Label({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Label };
diff --git a/packages/admin-ui/src/components/primitives/navigation-menu.tsx b/packages/admin-ui/src/components/primitives/navigation-menu.tsx
new file mode 100644
index 0000000000..c9b7c00b10
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/navigation-menu.tsx
@@ -0,0 +1,148 @@
+import * as React from "react";
+import { cva } from "class-variance-authority";
+import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui";
+
+import { cn } from "lib/utils";
+import { ChevronDownIcon } from "lucide-react";
+
+function NavigationMenu({
+ className,
+ children,
+ viewport = true,
+ ...props
+}: React.ComponentProps & {
+ viewport?: boolean;
+}) {
+ return (
+
+ {children}
+ {viewport && }
+
+ );
+}
+
+function NavigationMenuList({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function NavigationMenuItem({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+const navigationMenuTriggerStyle = cva(
+ "tw:group/navigation-menu-trigger tw:inline-flex tw:h-9 tw:w-max tw:items-center tw:justify-center tw:rounded-lg tw:px-2.5 tw:py-1.5 tw:text-sm tw:font-medium tw:transition-all tw:outline-none tw:hover:bg-muted tw:focus:bg-muted tw:focus-visible:ring-3 tw:focus-visible:ring-ring/50 tw:focus-visible:outline-1 tw:disabled:pointer-events-none tw:disabled:opacity-50 tw:data-popup-open:bg-muted/50 tw:data-popup-open:hover:bg-muted tw:data-open:bg-muted/50 tw:data-open:hover:bg-muted tw:data-open:focus:bg-muted"
+);
+
+function NavigationMenuTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}{" "}
+
+
+ );
+}
+
+function NavigationMenuContent({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function NavigationMenuViewport({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function NavigationMenuLink({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function NavigationMenuIndicator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+export {
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuContent,
+ NavigationMenuTrigger,
+ NavigationMenuLink,
+ NavigationMenuIndicator,
+ NavigationMenuViewport,
+ navigationMenuTriggerStyle
+};
diff --git a/packages/admin-ui/src/components/primitives/page.tsx b/packages/admin-ui/src/components/primitives/page.tsx
new file mode 100644
index 0000000000..ae46887b6b
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/page.tsx
@@ -0,0 +1,92 @@
+import * as React from "react";
+import { cn } from "lib/utils";
+import { TypographyH3, TypographyMuted } from "components/primitives/typography";
+
+/**
+ * Page-level container that applies the standard page padding and
+ * vertical rhythm. Use this as the outermost wrapper inside every
+ * route component rendered within `AiLayout`.
+ *
+ * ```tsx
+ *
+ *
+ *
+ *
+ * ```
+ */
+function PageContainer({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+/**
+ * Standardised page header with a title and optional description.
+ *
+ * Renders a semantic `` element containing:
+ * - A `` (TypographyH3) for the page heading
+ * - An optional `` (TypographyMuted) paragraph
+ *
+ * Supports both the shorthand props API and composition:
+ *
+ * ```tsx
+ * // Shorthand
+ *
+ *
+ * // Composed (for custom content)
+ *
+ * Store
+ * Browse AI packages.
+ * Custom action
+ *
+ * ```
+ */
+function PageHeader({
+ className,
+ title,
+ description,
+ children,
+ ...props
+}: React.ComponentProps<"header"> & {
+ /** Shorthand: renders a `` with this text. */
+ title?: React.ReactNode;
+ /** Shorthand: renders a `` below the title. */
+ description?: React.ReactNode;
+}) {
+ return (
+
+ {title && {title} }
+ {description && {description} }
+ {children}
+
+ );
+}
+
+/**
+ * The main heading for a page — wraps `TypographyH3` to ensure
+ * consistent page-level heading style across all routes.
+ */
+function PageTitle({ className, ...props }: React.ComponentProps<"h3">) {
+ return ;
+}
+
+/**
+ * A muted description paragraph that sits below ``.
+ * Wraps `TypographyMuted` and adds the standard `header-gap`
+ * spacing token and max-width constraint.
+ */
+function PageDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+
+ );
+}
+
+export { PageContainer, PageHeader, PageTitle, PageDescription };
diff --git a/packages/admin-ui/src/components/primitives/pagination.tsx b/packages/admin-ui/src/components/primitives/pagination.tsx
new file mode 100644
index 0000000000..6e8aac0c4d
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/pagination.tsx
@@ -0,0 +1,93 @@
+import * as React from "react";
+
+import { cn } from "lib/utils";
+import { Button } from "components/primitives/button";
+import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
+
+function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
+ return (
+
+ );
+}
+
+function PaginationContent({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ );
+}
+
+function PaginationItem({ ...props }: React.ComponentProps<"li">) {
+ return ;
+}
+
+type PaginationLinkProps = {
+ isActive?: boolean;
+} & Pick, "size"> &
+ React.ComponentProps<"a">;
+
+function PaginationLink({ className, isActive, size = "icon", ...props }: PaginationLinkProps) {
+ return (
+
+
+
+ );
+}
+
+function PaginationPrevious({
+ className,
+ text = "Previous",
+ ...props
+}: React.ComponentProps & { text?: string }) {
+ return (
+
+
+ {text}
+
+ );
+}
+
+function PaginationNext({
+ className,
+ text = "Next",
+ ...props
+}: React.ComponentProps & { text?: string }) {
+ return (
+
+ {text}
+
+
+ );
+}
+
+function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+
+ More pages
+
+ );
+}
+
+export {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious
+};
diff --git a/packages/admin-ui/src/components/primitives/progress.tsx b/packages/admin-ui/src/components/primitives/progress.tsx
new file mode 100644
index 0000000000..36ee78f753
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/progress.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import { Progress as ProgressPrimitive } from "radix-ui"
+
+import { cn } from "lib/utils"
+
+function Progress({
+ className,
+ value,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { Progress }
diff --git a/packages/admin-ui/src/components/primitives/radio-group.tsx b/packages/admin-ui/src/components/primitives/radio-group.tsx
new file mode 100644
index 0000000000..30f5834b61
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/radio-group.tsx
@@ -0,0 +1,42 @@
+import * as React from "react"
+import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
+
+import { cn } from "lib/utils"
+
+function RadioGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function RadioGroupItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { RadioGroup, RadioGroupItem }
diff --git a/packages/admin-ui/src/components/primitives/select.tsx b/packages/admin-ui/src/components/primitives/select.tsx
new file mode 100644
index 0000000000..324d0b197e
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/select.tsx
@@ -0,0 +1,171 @@
+"use client";
+
+import * as React from "react";
+import { Select as SelectPrimitive } from "radix-ui";
+
+import { cn } from "lib/utils";
+import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
+
+function Select({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SelectGroup({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SelectValue({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SelectTrigger({
+ className,
+ size = "default",
+ children,
+ ...props
+}: React.ComponentProps & {
+ size?: "sm" | "default";
+}) {
+ return (
+
+ {children}
+
+
+
+
+ );
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "item-aligned",
+ align = "center",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+function SelectLabel({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SelectItem({ className, children, ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ );
+}
+
+function SelectSeparator({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SelectScrollUpButton({ className, ...props }: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue
+};
diff --git a/packages/admin-ui/src/components/primitives/separator.tsx b/packages/admin-ui/src/components/primitives/separator.tsx
new file mode 100644
index 0000000000..66ccfd7364
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/separator.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import { Separator as SeparatorPrimitive } from "radix-ui"
+
+import { cn } from "lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/packages/admin-ui/src/components/primitives/sheet.tsx b/packages/admin-ui/src/components/primitives/sheet.tsx
new file mode 100644
index 0000000000..a640fc47f2
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/sheet.tsx
@@ -0,0 +1,107 @@
+import * as React from "react";
+import { Dialog as SheetPrimitive } from "radix-ui";
+
+import { cn } from "lib/utils";
+import { Button } from "components/primitives/button";
+import { XIcon } from "lucide-react";
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetClose({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetPortal({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function SheetOverlay({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ side?: "top" | "right" | "bottom" | "left";
+ showCloseButton?: boolean;
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+
+ Close
+
+
+ )}
+
+
+ );
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function SheetTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function SheetDescription({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription };
diff --git a/packages/admin-ui/src/components/primitives/sidebar.tsx b/packages/admin-ui/src/components/primitives/sidebar.tsx
new file mode 100644
index 0000000000..6e0a408685
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/sidebar.tsx
@@ -0,0 +1,716 @@
+"use client"
+
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Slot } from "radix-ui"
+
+import { useIsMobile } from "hooks/components/use-mobile"
+import { cn } from "lib/utils"
+import { Button } from "components/primitives/button"
+import { Input } from "components/primitives/input"
+import { Separator } from "components/primitives/separator"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "components/primitives/sheet"
+import { Skeleton } from "components/primitives/skeleton"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "components/primitives/tooltip"
+import { PanelLeftIcon } from "lucide-react"
+
+const SIDEBAR_STORAGE_KEY = "dappmanager-sidebar-collapsed"
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContextProps = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+function SidebarProvider({
+ defaultOpen = false,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}) {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ // Default to open (expanded) unless localStorage says it was collapsed.
+ const [_open, _setOpen] = React.useState(() => {
+ if (typeof window === "undefined") return defaultOpen
+ return localStorage.getItem(SIDEBAR_STORAGE_KEY) !== "true"
+ })
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // Persist collapsed state to localStorage
+ try {
+ localStorage.setItem(SIDEBAR_STORAGE_KEY, String(!openState))
+ } catch {
+ // Ignore storage errors (e.g. private browsing)
+ }
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function Sidebar({
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ dir,
+ ...props
+}: React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+}
+
+function SidebarTrigger({
+ className,
+ onClick,
+ ...props
+}: React.ComponentProps) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+ {
+ onClick?.(event)
+ toggleSidebar()
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ )
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+ return (
+
+ )
+}
+
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+ const Comp = (asChild ? Slot.Root : "div") as React.ElementType
+
+ return (
+ svg]:size-4 tw:[&>svg]:shrink-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+ const Comp = (asChild ? Slot.Root : "button") as React.ElementType
+
+ return (
+ svg]:size-4 tw:[&>svg]:shrink-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+const sidebarMenuButtonVariants = cva(
+ "tw:peer/menu-button tw:group/menu-button tw:flex tw:w-full tw:items-center tw:gap-2 tw:overflow-hidden tw:rounded-md tw:p-2 tw:text-left tw:text-sm tw:ring-sidebar-ring tw:outline-hidden tw:transition-[width,height,padding] tw:group-has-data-[sidebar=menu-action]/menu-item:pr-8 tw:group-data-[collapsible=icon]:size-8! tw:group-data-[collapsible=icon]:p-2! tw:hover:bg-sidebar-accent tw:hover:text-sidebar-accent-foreground tw:focus-visible:ring-2 tw:active:bg-sidebar-accent tw:active:text-sidebar-accent-foreground tw:disabled:pointer-events-none tw:disabled:opacity-50 tw:aria-disabled:pointer-events-none tw:aria-disabled:opacity-50 tw:data-[state=open]:hover:bg-sidebar-accent tw:data-[state=open]:hover:text-sidebar-accent-foreground tw:data-[active=true]:bg-sidebar-accent tw:data-[active=true]:font-medium tw:data-[active=true]:text-sidebar-accent-foreground tw:[&>span:last-child]:truncate tw:[&_svg]:size-4 tw:[&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "tw:bg-sidebar-primary-foreground tw:hover:bg-sidebar-accent tw:hover:text-sidebar-accent-foreground",
+ outline:
+ "tw:bg-background tw:shadow-[0_0_0_1px_hsl(var(--sidebar-border))] tw:hover:bg-sidebar-accent tw:hover:text-sidebar-accent-foreground tw:hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "tw:h-8 tw:text-sm",
+ sm: "tw:h-7 tw:text-xs",
+ lg: "tw:h-12 tw:text-sm tw:group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+} & VariantProps) {
+ const Comp = (asChild ? Slot.Root : "button") as React.ElementType
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ if (state === "expanded" ){
+ return button
+ }
+
+ return (
+
+ {button}
+
+
+ )
+}
+
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+}) {
+ const Comp = (asChild ? Slot.Root : "button") as React.ElementType
+
+ return (
+ svg]:size-4 tw:[&>svg]:shrink-0",
+ showOnHover &&
+ "tw:group-focus-within/menu-item:opacity-100 tw:group-hover/menu-item:opacity-100 tw:peer-data-[active=true]/menu-button:text-sidebar-accent-foreground tw:data-[state=open]:opacity-100 tw:md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showIcon?: boolean
+}) {
+ // Random width between 50 to 90%.
+ const [width] = React.useState(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ })
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubButton({
+ asChild = false,
+ size = "md",
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+}) {
+ const Comp = (asChild ? Slot.Root : "a") as React.ElementType
+
+ return (
+ span:last-child]:truncate tw:[&>svg]:size-4 tw:[&>svg]:shrink-0 tw:[&>svg]:text-sidebar-accent-foreground",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/packages/admin-ui/src/components/primitives/skeleton.tsx b/packages/admin-ui/src/components/primitives/skeleton.tsx
new file mode 100644
index 0000000000..fe4196a5d4
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/skeleton.tsx
@@ -0,0 +1,10 @@
+import * as React from "react";
+import { cn } from "lib/utils";
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/packages/admin-ui/src/components/primitives/slider.tsx b/packages/admin-ui/src/components/primitives/slider.tsx
new file mode 100644
index 0000000000..4b07048381
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/slider.tsx
@@ -0,0 +1,46 @@
+import * as React from "react";
+import { Slider as SliderPrimitive } from "radix-ui";
+
+import { cn } from "lib/utils";
+
+function Slider({
+ className,
+ defaultValue,
+ value,
+ min = 0,
+ max = 100,
+ ...props
+}: React.ComponentProps) {
+ const _values = React.useMemo(
+ () => (Array.isArray(value) ? value : Array.isArray(defaultValue) ? defaultValue : [min, max]),
+ [value, defaultValue, min, max]
+ );
+
+ return (
+
+
+
+
+ {Array.from({ length: _values.length }, (_, index) => (
+
+ ))}
+
+ );
+}
+
+export { Slider };
diff --git a/packages/admin-ui/src/components/primitives/sonner.tsx b/packages/admin-ui/src/components/primitives/sonner.tsx
new file mode 100644
index 0000000000..2bfe4f4010
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/sonner.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { useTheme } from "components/ThemeProvider"
+import { Toaster as Sonner, type ToasterProps } from "sonner"
+import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ ),
+ info: (
+
+ ),
+ warning: (
+
+ ),
+ error: (
+
+ ),
+ loading: (
+
+ ),
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--success-bg": "var(--popover)",
+ "--success-text": "var(--success)",
+ "--success-border": "var(--border)",
+ "--error-bg": "var(--popover)",
+ "--error-text": "var(--destructive)",
+ "--error-border": "var(--border)",
+ "--warning-bg": "var(--popover)",
+ "--warning-text": "var(--caution)",
+ "--warning-border": "var(--border)",
+ "--info-bg": "var(--popover)",
+ "--info-text": "var(--primary)",
+ "--info-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/packages/admin-ui/src/components/primitives/spinner.tsx b/packages/admin-ui/src/components/primitives/spinner.tsx
new file mode 100644
index 0000000000..7fbd7d2f1c
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/spinner.tsx
@@ -0,0 +1,11 @@
+import * as React from "react";
+import { cn } from "lib/utils";
+import { Loader2Icon } from "lucide-react";
+
+function Spinner({ className, ...props }: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Spinner };
diff --git a/packages/admin-ui/src/components/primitives/switch.tsx b/packages/admin-ui/src/components/primitives/switch.tsx
new file mode 100644
index 0000000000..40301a8505
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/switch.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as React from "react";
+import { Switch as SwitchPrimitive } from "radix-ui";
+
+import { cn } from "lib/utils";
+
+function Switch({ className, ...props }: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+export { Switch };
diff --git a/packages/admin-ui/src/components/primitives/tooltip.tsx b/packages/admin-ui/src/components/primitives/tooltip.tsx
new file mode 100644
index 0000000000..678579d4bd
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/tooltip.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import * as React from "react";
+import { Tooltip as TooltipPrimitive } from "radix-ui";
+
+import { cn } from "lib/utils";
+
+function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps) {
+ return ;
+}
+
+function Tooltip({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function TooltipTrigger({ ...props }: React.ComponentProps) {
+ return ;
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ );
+}
+
+export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
diff --git a/packages/admin-ui/src/components/primitives/typography.tsx b/packages/admin-ui/src/components/primitives/typography.tsx
new file mode 100644
index 0000000000..43f3913314
--- /dev/null
+++ b/packages/admin-ui/src/components/primitives/typography.tsx
@@ -0,0 +1,123 @@
+import * as React from "react";
+import { cn } from "lib/utils";
+
+function TypographyH1({ className, ...props }: React.ComponentProps<"h1">) {
+ return (
+
+ );
+}
+
+function TypographyH2({ className, ...props }: React.ComponentProps<"h2">) {
+ return (
+
+ );
+}
+
+function TypographyH3({ className, ...props }: React.ComponentProps<"h3">) {
+ return (
+
+ );
+}
+
+function TypographyH4({ className, ...props }: React.ComponentProps<"h4">) {
+ return (
+
+ );
+}
+
+function TypographyP({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+
+ );
+}
+
+function TypographyBlockquote({ className, ...props }: React.ComponentProps<"blockquote">) {
+ return (
+
+ );
+}
+
+function TypographyInlineCode({ className, ...props }: React.ComponentProps<"code">) {
+ return (
+
+ );
+}
+
+function TypographyLead({ className, ...props }: React.ComponentProps<"p">) {
+ return
;
+}
+
+function TypographyLarge({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function TypographySmall({ className, ...props }: React.ComponentProps<"small">) {
+ return (
+
+ );
+}
+
+function TypographyMuted({ className, ...props }: React.ComponentProps<"p">) {
+ return
;
+}
+
+export {
+ TypographyH1,
+ TypographyH2,
+ TypographyH3,
+ TypographyH4,
+ TypographyP,
+ TypographyBlockquote,
+ TypographyInlineCode,
+ TypographyLead,
+ TypographyLarge,
+ TypographySmall,
+ TypographyMuted
+};
diff --git a/packages/admin-ui/src/components/sectionNavigator.scss b/packages/admin-ui/src/components/sectionNavigator.scss
index dded0a90b1..9555fe093f 100644
--- a/packages/admin-ui/src/components/sectionNavigator.scss
+++ b/packages/admin-ui/src/components/sectionNavigator.scss
@@ -1,50 +1,52 @@
-.horizontal-navbar {
- display: flex;
- border-bottom: 1px solid var(--color-light-border);
- margin-bottom: 1rem;
- padding: 0.5rem 0;
- overflow-x: auto;
+.legacy-bootstrap {
+ .horizontal-navbar {
+ display: flex;
+ border-bottom: 1px solid var(--color-light-border);
+ margin-bottom: 1rem;
+ padding: 0.5rem 0;
+ overflow-x: auto;
- .item-container {
- background-color: transparent;
- border: none;
- padding-bottom: 0.3rem;
- padding-left: 0;
- font-size: 1rem;
- &:not(:last-child) {
- padding-right: 1rem;
- margin-right: 1rem;
- border-right: var(--border-style);
- }
- &:hover {
- color: inherit;
- }
-
- // Remove the border (outline) on click
- &:focus {
- outline: none;
- }
- }
- .item {
- color: #7b7d7f;
- border: none;
- padding-bottom: 0.3rem;
- cursor: pointer;
- &.active {
- color: #212529;
- border-bottom: 5px solid var(--dappnode-strong-main-color);
- }
- }
-
- // Small variant
- &.sm {
- padding-bottom: 0.25rem;
.item-container {
- font-size: 0.9rem;
+ background-color: transparent;
+ border: none;
+ padding-bottom: 0.3rem;
+ padding-left: 0;
+ font-size: 1rem;
+ &:not(:last-child) {
+ padding-right: 1rem;
+ margin-right: 1rem;
+ border-right: var(--border-style);
+ }
+ &:hover {
+ color: inherit;
+ }
+
+ // Remove the border (outline) on click
+ &:focus {
+ outline: none;
+ }
}
.item {
+ color: #7b7d7f;
+ border: none;
+ padding-bottom: 0.3rem;
+ cursor: pointer;
&.active {
- border-bottom-width: 3px;
+ color: #212529;
+ border-bottom: 5px solid var(--dappnode-strong-main-color);
+ }
+ }
+
+ // Small variant
+ &.sm {
+ padding-bottom: 0.25rem;
+ .item-container {
+ font-size: 0.9rem;
+ }
+ .item {
+ &.active {
+ border-bottom-width: 3px;
+ }
}
}
}
diff --git a/packages/admin-ui/src/components/sidebar/SideBar.tsx b/packages/admin-ui/src/components/sidebar/SideBar.tsx
index f18f910d01..6177a3f196 100644
--- a/packages/admin-ui/src/components/sidebar/SideBar.tsx
+++ b/packages/admin-ui/src/components/sidebar/SideBar.tsx
@@ -37,8 +37,11 @@ export default function SideBar({ screenWidth }: { screenWidth: number }) {
.filter((item) => item.show === true)
.map((item) => {
const basePath = item.href.split("/")[0];
- const baseLocationPath = location.pathname.substring(1).split("/")[0];
- const isActive = baseLocationPath === basePath;
+ // Sidebar renders inside /legacy, so location is /legacy//...
+ // Compare the first href segment against the second path segment
+ const pathSegments = location.pathname.substring(1).split("/");
+ const currentPage = pathSegments[0] === "legacy" ? pathSegments[1] : pathSegments[0];
+ const isActive = currentPage === basePath;
return (
navigate("/notifications/inbox")} className="tn-dropdown tn-dropdown-toggle">
+ navigate(withLegacyBase(notificationsRelativePath))} className="tn-dropdown tn-dropdown-toggle">
{newNotifications &&
}
diff --git a/packages/admin-ui/src/components/topbar/dropdownMenus/dropdown.scss b/packages/admin-ui/src/components/topbar/dropdownMenus/dropdown.scss
index e6dcc2d885..4f7da633c3 100644
--- a/packages/admin-ui/src/components/topbar/dropdownMenus/dropdown.scss
+++ b/packages/admin-ui/src/components/topbar/dropdownMenus/dropdown.scss
@@ -1,131 +1,135 @@
-.tn-dropdown {
- position: relative;
+.legacy-bootstrap {
+ .tn-dropdown {
+ position: relative;
- .icon-bubble {
- position: absolute;
- right: -5px;
- top: -5px;
- background: transparent;
- border-radius: var(--bubble-size);
- height: var(--bubble-size);
- width: var(--bubble-size);
+ .icon-bubble {
+ position: absolute;
+ right: -5px;
+ top: -5px;
+ background: transparent;
+ border-radius: var(--bubble-size);
+ height: var(--bubble-size);
+ width: var(--bubble-size);
- &.success {
- background: var(--success-color);
- }
- &.warning {
- background: var(--warning-color);
- }
- &.danger {
- background: var(--danger-color);
+ &.success {
+ background: var(--success-color);
+ }
+ &.warning {
+ background: var(--warning-color);
+ }
+ &.danger {
+ background: var(--danger-color);
+ }
}
- }
- > .menu {
- /* Does not need a z-index, it has the topbar z-index */
- background-color: white;
- border: var(--border-style);
- border-radius: 0.5rem;
- padding: 0.5rem 0;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- /* Positioning */
- position: absolute;
- top: 2.5rem;
- /* Display control */
- display: none;
- /* Sizing */
- width: 25rem;
- right: 0;
- max-height: 30rem;
- overflow-y: auto;
+ > .menu {
+ /* Does not need a z-index, it has the topbar z-index */
+ background-color: white;
+ border: var(--border-style);
+ border-radius: 0.5rem;
+ padding: 0.5rem 0;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ /* Positioning */
+ position: absolute;
+ top: 2.5rem;
+ /* Display control */
+ display: none;
+ /* Sizing */
+ width: 25rem;
+ right: 0;
+ max-height: 30rem;
+ overflow-y: auto;
- &.show {
- display: block;
- }
+ &.show {
+ display: block;
+ }
- @media (max-width: 40rem) {
- width: calc(100vw - 1rem);
- max-width: 16rem;
- }
+ @media (max-width: 40rem) {
+ width: calc(100vw - 1rem);
+ max-width: 16rem;
+ }
- /* Menu items */
- > div {
- padding: 0.75rem 1rem;
- overflow-wrap: break-word;
+ /* Menu items */
+ > div {
+ padding: 0.75rem 1rem;
+ overflow-wrap: break-word;
- .title {
- display: flex;
- align-items: center;
- .text {
- flex: auto;
- }
- .title-actions {
+ .title {
display: flex;
align-items: center;
- gap: 0.5rem;
- .help {
- font-size: 140%;
+ .text {
+ flex: auto;
+ }
+ .title-actions {
display: flex;
- color: #6c757d;
+ align-items: center;
+ gap: 0.5rem;
+ .help {
+ font-size: 140%;
+ display: flex;
+ color: #6c757d;
+ }
}
}
}
- }
- > div:not(:last-child) {
- border-bottom: var(--border-style);
- }
- > .header {
- color: #6c757d;
- font-weight: 600;
- font-size: 1.1rem;
- padding: 0.75rem 1rem;
- background-color: rgba(0, 0, 0, 0.02);
- }
- > .placeholder {
- color: #6c757d;
- opacity: 0.8;
- text-align: center;
- padding: 1rem;
- }
- .progress {
- margin: 0.5rem 0;
- }
- }
-
- &.installer {
- > .menu {
- right: -3rem;
- > div {
+ > div:not(:last-child) {
+ border-bottom: var(--border-style);
+ }
+ > .header {
+ color: #6c757d;
+ font-weight: 600;
+ font-size: 1.1rem;
+ padding: 0.75rem 1rem;
+ background-color: rgba(0, 0, 0, 0.02);
+ }
+ > .placeholder {
+ color: #6c757d;
+ opacity: 0.8;
+ text-align: center;
padding: 1rem;
}
+ .progress {
+ margin: 0.5rem 0;
+ }
}
- .progress-logs {
- width: 100%;
- margin: 0.5rem 0;
- }
- }
- &.dappnodeidentity {
- > .menu {
- width: 20rem;
+ &.installer {
+ > .menu {
+ right: -3rem;
+ > div {
+ padding: 1rem;
+ }
+ }
+
+ .progress-logs {
+ width: 100%;
+ margin: 0.5rem 0;
+ }
}
+ &.dappnodeidentity {
+ > .menu {
+ width: 20rem;
+ }
- .sign-out {
- cursor: pointer;
- color: #dc3545 !important;
+ .sign-out {
+ cursor: pointer;
+ color: #dc3545 !important;
+ }
}
- }
- // Regulate size of toggle icons
- &.notifications .tn-dropdown-toggle svg {
- font-size: 1.3rem;
+ // Regulate size of toggle icons
+ &.notifications .tn-dropdown-toggle svg {
+ font-size: 1.3rem;
+ }
}
}
-.tn-dropdown-toggle {
- /* flex automatically centers the toggle icons */
- padding: 0.25rem;
- display: flex;
- opacity: 0.6;
- cursor: pointer;
+.legacy-bootstrap {
+ .tn-dropdown-toggle {
+ /* flex automatically centers the toggle icons */
+ padding: 0.25rem;
+ display: flex;
+ opacity: 0.6;
+ cursor: pointer;
+ }
}
diff --git a/packages/admin-ui/src/dappnode_colors.scss b/packages/admin-ui/src/dappnode_colors.scss
index 002b4ade06..c14816210e 100644
--- a/packages/admin-ui/src/dappnode_colors.scss
+++ b/packages/admin-ui/src/dappnode_colors.scss
@@ -1,53 +1,7 @@
-:root {
- --dappnode-white-color: #fff;
- --dappnode-strong-main-color: #00b1f4;
- --dappnode-darker-main-color: #007dfc;
- --dappnode-shadow-main-color: #06d4e7;
- --dappnode-light-main-color: #a0bdbb;
- --dappnode-gray-main-color: #748888;
- --dappnode-links-color: #00b1f4;
- --dappnode-links-darker-color: #007dfc;
- --dappnode-complimentary-color: #bc2f39;
- // Border colors
- --border-color: #e5e5e5;
- --color-light-border: #e5e5e5;
- --color-dark-border: #616161;
- --border-style-light: var(--border-size) solid var(--color-light-border);
- --border-style-dark: var(--border-size) solid var(--color-dark-border);
-
- // LIGHT mode
- --color-light-background-sidebar-topbar: white; // Used by sidebar and topbar
- --color-light-background-main: #f7f9f9; // Used by main
-
- // DARK mode
- --color-dark-background-sidebar: #212121; // Used by sidebar
- --color-dark-background-topbar: #313131; // Used by topbar
- --color-dark-background-main: #313131; // Used by main
- --color-dark-maintext: #ffffff; // Used by main
- --color-dark-secondarytext: #dddddd; // Used by main
- --color-dark-tertiarytext: #cccccc; // Used by main
- --color-dark-quaternarytext: #c1c1c1; // Used by main
- --color-dark-card: #414141; // Used by cards
- --color-dark-card-hover: #6a6a6a; // Used by cards
- --color-dark-danger-background: #8d2222; // Used by danger alerts
-
- // Buttons
- --dappnode-white-color: #fff; // Used by buttons
-
- // Alert colors
- /* Danger color from the complimentary color of #2fbcb2 (dappnode-color) */
- /* Warning color from From https://www.colorcombos.com/color-schemes/429/ColorCombo429.html */
- --danger-color: var(--dappnode-complimentary-color);
- --warning-color: #ffcc00;
- --success-color: var(--dappnode-strong-main-color);
- --success-green-color: #34a853;
- /* Shortcuts */
- --dappnode-color: var(--dappnode-strong-main-color);
- /* Text colors */
- --light-text-color: #757575;
- /* Opacity shades */
- --opacity-soft: 0.6;
-}
+// CSS custom property definitions have been extracted to
+// styles/dappnode-color-vars.scss (imported at root level in
+// bootstrap-scoped.scss) so they land on :root even when this file
+// is imported inside the .legacy-bootstrap scope.
.color-danger {
color: var(--danger-color);
diff --git a/packages/admin-ui/src/hooks/components/use-mobile.ts b/packages/admin-ui/src/hooks/components/use-mobile.ts
new file mode 100644
index 0000000000..2b0fe1dfef
--- /dev/null
+++ b/packages/admin-ui/src/hooks/components/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/packages/admin-ui/src/hooks/useHandleNotificationsPkg.ts b/packages/admin-ui/src/hooks/useHandleNotificationsPkg.ts
index 45d254e481..9d97398eee 100644
--- a/packages/admin-ui/src/hooks/useHandleNotificationsPkg.ts
+++ b/packages/admin-ui/src/hooks/useHandleNotificationsPkg.ts
@@ -29,22 +29,15 @@ export function useHandleNotificationsPkg() {
}
}, [notificationsStatusRequest.data]);
- const startStopNotifications = useCallback(async (): Promise => {
+ /**
+ * Core start/stop logic without any confirmation dialog.
+ * Used by the new UI which handles confirmation externally.
+ */
+ const startStopNotificationsNoConfirm = useCallback(async (): Promise => {
try {
if (isInstalled && notificationsPkg) {
const notificationsRunning = notRunningServices.length === 0;
- if (notificationsRunning) {
- await new Promise((resolve) => {
- confirm({
- title: `Pause notifications package`,
- text: `Attention, the notifications package may alert you to critical issues if they arise. Pausing this package could result in missing important notifications.`,
- label: "Pause",
- onClick: resolve
- });
- });
- }
-
await withToast(
continueIfCalleDisconnected(
() =>
@@ -69,9 +62,37 @@ export function useHandleNotificationsPkg() {
}
}, [isInstalled, notificationsPkg, notRunningServices, notificationsStatusRequest]);
+ /**
+ * Start/stop with legacy confirmation dialog.
+ * Used by legacy pages (Settings.tsx, EnableNotifications.tsx).
+ */
+ const startStopNotifications = useCallback(async (): Promise => {
+ try {
+ if (isInstalled && notificationsPkg) {
+ const notificationsRunning = notRunningServices.length === 0;
+
+ if (notificationsRunning) {
+ await new Promise((resolve) => {
+ confirm({
+ title: `Pause notifications package`,
+ text: `Attention, the notifications package may alert you to critical issues if they arise. Pausing this package could result in missing important notifications.`,
+ label: "Pause",
+ onClick: resolve
+ });
+ });
+ }
+
+ await startStopNotificationsNoConfirm();
+ }
+ } catch (e) {
+ console.error(`Error on start/stop notifications package: ${e}`);
+ }
+ }, [isInstalled, notificationsPkg, notRunningServices, startStopNotificationsNoConfirm]);
+
return {
isLoading,
isRunning,
- startStopNotifications
+ startStopNotifications,
+ startStopNotificationsNoConfirm
};
}
diff --git a/packages/admin-ui/src/index.tsx b/packages/admin-ui/src/index.tsx
index 68c0834b50..ab54a16f5d 100755
--- a/packages/admin-ui/src/index.tsx
+++ b/packages/admin-ui/src/index.tsx
@@ -9,13 +9,12 @@ import { cleanObj } from "utils/objects";
// Init css
import "react-toastify/dist/ReactToastify.css";
-// Boostrap loaders
-import "bootstrap/dist/css/bootstrap.min.css";
+// Bootstrap — scoped under .legacy-bootstrap so it only affects legacy /legacy/* routes
+import "./styles/bootstrap-scoped.scss";
// Custom styles
-import "./dappnode_styles.scss";
-import "./dappnode_colors.scss";
-import "./light_dark.scss";
-import "./layout.scss";
+
+// Tailwind CSS v4 — isolated, prefixed, no preflight
+import "./styles/tailwind.css";
// PWA
import { PwaInstallProvider } from "pages/system/components/App/PwaInstallContext";
// Grafana Faro for frontend monitoring and tracing (initialized paused until consent)
diff --git a/packages/admin-ui/src/layouts/DecorativeBackground.tsx b/packages/admin-ui/src/layouts/DecorativeBackground.tsx
new file mode 100644
index 0000000000..fb7837c659
--- /dev/null
+++ b/packages/admin-ui/src/layouts/DecorativeBackground.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+
+/**
+ * Decorative background layer with gradient orbs and dot-grid overlay.
+ *
+ * Designed to sit behind page content as an absolute-positioned layer.
+ * Used by `SectionLayout` and `NewPageLayout` to provide a consistent
+ * visual feel across all new pages.
+ *
+ * Orb opacities are reduced in dark mode so the colours blend into the
+ * dark background without overwhelming the UI.
+ *
+ * This component is purely presentational — it renders no interactive
+ * elements and is hidden from assistive technology.
+ */
+export function DecorativeBackground() {
+ return (
+
+ {/* Orb — top-left (purple) */}
+
+ {/* Orb — top-right (orange) */}
+
+ {/* Orb — bottom-left (blue) */}
+
+ {/* Orb — bottom-right (pink) */}
+
+ {/* Subtle dot-grid overlay */}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/layouts/LegacyStakingLayout.tsx b/packages/admin-ui/src/layouts/LegacyStakingLayout.tsx
new file mode 100644
index 0000000000..662fb1039b
--- /dev/null
+++ b/packages/admin-ui/src/layouts/LegacyStakingLayout.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+import { Outlet } from "react-router-dom";
+import SideBar from "components/sidebar/SideBar";
+import { TopBar } from "components/topbar/TopBar";
+import NotificationsMain from "components/NotificationsMain";
+import ErrorBoundary from "components/ErrorBoundary";
+import Welcome from "components/welcome/Welcome";
+import Smooth from "components/Smooth";
+import { PwaPermissionsAlert, PwaPermissionsModal } from "components/PwaPermissions";
+import { LocalProxyBanner } from "pages/wifi/components/localProxying/LocalProxyBanner";
+import { ToastContainer } from "react-toastify";
+import { AppContextIface } from "types";
+
+/**
+ * Layout wrapper for all legacy pages under /staking/*.
+ * Includes the sidebar, topbar, and all legacy chrome.
+ * Bootstrap + legacy SCSS remain the styling source for these pages.
+ * Do not add Tailwind classes or shadcn components here.
+ */
+export function LegacyStakingLayout({
+ screenWidth,
+ username,
+ appContext
+}: {
+ screenWidth: number;
+ username: string;
+ appContext: AppContextIface;
+}) {
+ return (
+
+
+
+
+
+ {/* Legacy non-page components */}
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/layouts/SectionLayout.tsx b/packages/admin-ui/src/layouts/SectionLayout.tsx
new file mode 100644
index 0000000000..7e5a6a736c
--- /dev/null
+++ b/packages/admin-ui/src/layouts/SectionLayout.tsx
@@ -0,0 +1,188 @@
+import React from "react";
+import { useNavigate, useLocation, Link } from "react-router-dom";
+import { Home } from "lucide-react";
+import dappnodeLogo from "img/dappnode-logo-only.png";
+import {
+ SidebarProvider,
+ Sidebar,
+ SidebarHeader,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuItem,
+ SidebarMenuButton,
+ SidebarInset,
+ SidebarTrigger,
+ useSidebar
+} from "components/primitives/sidebar";
+import {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator
+} from "components/primitives/breadcrumb";
+import { ThemeToggle } from "components/ThemeToggle";
+import { Toaster } from "components/primitives/sonner";
+import { DecorativeBackground } from "./DecorativeBackground";
+
+export interface NavItem {
+ label: string;
+ icon: React.ComponentType;
+ path: string;
+}
+
+export interface SectionLayoutProps {
+ /** Human-readable section name shown in sidebar subtitle & breadcrumb root (e.g. "AI"). */
+ sectionLabel: string;
+ /** Base path for this section (e.g. "/ai"). Used for breadcrumb root link. */
+ basePath: string;
+ /** Navigation items rendered in the sidebar. */
+ navItems: NavItem[];
+ /** Page content — typically a `` block. */
+ children: React.ReactNode;
+}
+
+/* ── Breadcrumb helper ──────────────────────────────────────────────── */
+
+function getBreadcrumbItems(pathname: string, basePath: string): { label: string; to: string }[] {
+ // Strip the basePath prefix, then split into segments
+ const trimmed = pathname.startsWith(basePath) ? pathname.slice(basePath.length) : pathname;
+ const segments = trimmed.split("/").filter(Boolean);
+ // Normalize basePath so "/" doesn't produce double slashes
+ const prefix = basePath === "/" ? "" : basePath;
+
+ return segments.map((segment, index) => ({
+ label: decodeURIComponent(segment),
+ to: `${prefix}/${segments.slice(0, index + 1).join("/")}`
+ }));
+}
+
+/* ── Sidebar brand header (collapse-aware) ──────────────────────────── */
+
+function SidebarBrandHeader({ sectionLabel, onNavigateHome }: { sectionLabel: string; onNavigateHome: () => void }) {
+ const { state } = useSidebar();
+ const isCollapsed = state === "collapsed";
+
+ return (
+
+
+
+
+
+
+
+ {!isCollapsed && (
+
+ Dappnode
+ {sectionLabel}
+
+ )}
+
+
+
+
+ );
+}
+
+/* ── Layout ─────────────────────────────────────────────────────────── */
+
+export function SectionLayout({ sectionLabel, basePath, navItems, children }: SectionLayoutProps) {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const breadcrumbItems = getBreadcrumbItems(location.pathname, basePath);
+
+ return (
+
+
+
+ navigate("/")} />
+
+ {/* Navigation */}
+
+
+ Navigation
+
+ {navItems.map((item) => {
+ const isActive = location.pathname === item.path || location.pathname.startsWith(item.path + "/");
+ return (
+
+ navigate(item.path)} tooltip={item.label}>
+
+ {item.label}
+
+
+ );
+ })}
+
+
+
+ {basePath !== "/" && (
+
+
+
+ navigate("/")} tooltip="Back to Home">
+
+ Back to Home
+
+
+
+
+ )}
+
+
+
+
+ {/* Decorative orb background behind page content */}
+
+
+ {/* Top bar */}
+
+
+
+ {/* Responsive breadcrumb */}
+
+ {breadcrumbItems.length > 0 && (
+
+
+
+ {sectionLabel}
+
+
+ {breadcrumbItems.map((item, index) => {
+ const isLast = index === breadcrumbItems.length - 1;
+ return (
+
+
+
+ {isLast ? (
+ {item.label}
+ ) : (
+
+ {item.label}
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Dark / light mode toggle */}
+
+
+
+ {/* Page content */}
+ {children}
+
+ {/* Toast notifications */}
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/layouts/index.ts b/packages/admin-ui/src/layouts/index.ts
new file mode 100644
index 0000000000..3fa367b045
--- /dev/null
+++ b/packages/admin-ui/src/layouts/index.ts
@@ -0,0 +1,3 @@
+export { SectionLayout } from "./SectionLayout";
+export type { NavItem, SectionLayoutProps } from "./SectionLayout";
+export { DecorativeBackground } from "./DecorativeBackground";
diff --git a/packages/admin-ui/src/lib/utils.ts b/packages/admin-ui/src/lib/utils.ts
new file mode 100644
index 0000000000..80d70bb07e
--- /dev/null
+++ b/packages/admin-ui/src/lib/utils.ts
@@ -0,0 +1,7 @@
+// src/lib/utils.ts
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
\ No newline at end of file
diff --git a/packages/admin-ui/src/light_dark.scss b/packages/admin-ui/src/light_dark.scss
index 6f42682878..8745988df4 100644
--- a/packages/admin-ui/src/light_dark.scss
+++ b/packages/admin-ui/src/light_dark.scss
@@ -191,7 +191,7 @@ body.dark {
.nav a,
.nav .sidenav-item,
.sidebar-media-footer {
- color: var(--color-dark-secondarytext);
+ color: var(--color-dark-secondarytext) !important;
svg {
opacity: 1;
}
diff --git a/packages/admin-ui/src/pages-new/ai/AiLayout.tsx b/packages/admin-ui/src/pages-new/ai/AiLayout.tsx
new file mode 100644
index 0000000000..4b56e7e48d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/AiLayout.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import { Routes, Route, Navigate } from "react-router-dom";
+import { ShoppingBag, Package, Globe } from "lucide-react";
+import { SectionLayout, NavItem } from "layouts";
+import { PackagesPage, PackageDetailPage, StorePage } from "pages-new/packages";
+import { NexusPage } from "./nexus/NexusPage";
+import { nexusRelativePath } from "./nexus/data";
+import { BannerNotifications } from "../home/BannerNotifications";
+import { aiPackagesConfig } from "./packagesConfig";
+
+/* ── Navigation items ───────────────────────────────────────────────── */
+
+const navItems: NavItem[] = [
+ { label: "Packages", icon: Package, path: aiPackagesConfig.packagesPath },
+ { label: "Store", icon: ShoppingBag, path: aiPackagesConfig.storePath },
+ { label: "Nexus", icon: Globe, path: nexusRelativePath }
+];
+
+/* ── Layout ─────────────────────────────────────────────────────────── */
+
+export function AiLayout() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/OverviewPage.tsx b/packages/admin-ui/src/pages-new/ai/OverviewPage.tsx
new file mode 100644
index 0000000000..eb08c82cfa
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/OverviewPage.tsx
@@ -0,0 +1,929 @@
+import React, { useState } from "react";
+import {
+ Info,
+ AlertTriangle,
+ CheckCircle2,
+ ChevronsUpDown,
+ Star,
+ FileText,
+ Image,
+ Inbox,
+ Sparkles
+} from "lucide-react";
+import { toast } from "sonner";
+
+/* ── Existing primitives ─────────────────────────────────────────── */
+import { Button } from "components/primitives/button";
+import { PageContainer, PageHeader } from "components/primitives/page";
+import {
+ Card,
+ ClickableCard,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter
+} from "components/primitives/card";
+import { Input } from "components/primitives/input";
+import { Separator } from "components/primitives/separator";
+import {
+ Sheet,
+ SheetTrigger,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetDescription,
+ SheetFooter,
+ SheetClose
+} from "components/primitives/sheet";
+import { Skeleton } from "components/primitives/skeleton";
+import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "components/primitives/tooltip";
+
+/* ── New primitives ──────────────────────────────────────────────── */
+import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "components/primitives/accordion";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { Badge } from "components/primitives/badge";
+import {
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext
+} from "components/primitives/carousel";
+import { Checkbox } from "components/primitives/checkbox";
+import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "components/primitives/collapsible";
+import {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuSeparator,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioGroup,
+ ContextMenuRadioItem,
+ ContextMenuSub,
+ ContextMenuSubTrigger,
+ ContextMenuSubContent,
+ ContextMenuLabel
+} from "components/primitives/context-menu";
+import {
+ Dialog,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+ DialogClose
+} from "components/primitives/dialog";
+import {
+ Drawer,
+ DrawerTrigger,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerClose
+} from "components/primitives/drawer";
+import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyMedia } from "components/primitives/empty";
+import { Field, FieldLabel, FieldDescription, FieldError, FieldSet } from "components/primitives/field";
+import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription as ItemDesc } from "components/primitives/item";
+import {
+ Pagination,
+ PaginationContent,
+ PaginationItem,
+ PaginationLink,
+ PaginationPrevious,
+ PaginationNext,
+ PaginationEllipsis
+} from "components/primitives/pagination";
+import { Progress } from "components/primitives/progress";
+import { RadioGroup, RadioGroupItem } from "components/primitives/radio-group";
+import {
+ Select,
+ SelectTrigger,
+ SelectValue,
+ SelectContent,
+ SelectItem,
+ SelectGroup,
+ SelectLabel
+} from "components/primitives/select";
+import { Slider } from "components/primitives/slider";
+import { Spinner } from "components/primitives/spinner";
+import { Switch } from "components/primitives/switch";
+import { Label } from "components/primitives/label";
+import {
+ TypographyH1,
+ TypographyH2,
+ TypographyH3,
+ TypographyH4,
+ TypographyP,
+ TypographyBlockquote,
+ TypographyInlineCode,
+ TypographyLead,
+ TypographyLarge,
+ TypographySmall,
+ TypographyMuted
+} from "components/primitives/typography";
+
+/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
+/* Helpers */
+/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
+
+/** Section wrapper used for every component demo block */
+function Section({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+ );
+}
+
+/** Wraps demo items in a flex row with wrapping */
+function Row({ children, className }: { children: React.ReactNode; className?: string }) {
+ return {children}
;
+}
+
+/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
+/* Overview Page */
+/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
+
+export function OverviewPage() {
+ /* local state for interactive demos */
+ const [progress, setProgress] = useState(45);
+ const [sliderValue, setSliderValue] = useState([33]);
+ const [checkboxA, setCheckboxA] = useState(true);
+ const [checkboxB, setCheckboxB] = useState(false);
+ const [switchOn, setSwitchOn] = useState(true);
+ const [radioValue, setRadioValue] = useState("option-a");
+ const [collapsibleOpen, setCollapsibleOpen] = useState(false);
+ const [ctxCheck, setCtxCheck] = useState(true);
+ const [ctxRadio, setCtxRadio] = useState("pedro");
+
+ return (
+
+
+ {/* Page header */}
+
+
+
+
+ {/* ── Typography ─────────────────────────────────────────── */}
+
+ Heading 1
+ Heading 2
+ Heading 3
+ Heading 4
+
+ This is a paragraph. The quick brown fox jumps over the lazy dog. Typography sets the tone for the entire
+ interface and should feel balanced.
+
+ This is a lead paragraph — slightly larger and muted.
+ Large text for emphasis
+ Small text for fine print
+ Muted text for secondary information
+ "Decentralise everything" — this is a blockquote.
+
+ Use inline code for code references.
+
+
+
+
+
+ {/* ── Button ─────────────────────────────────────────────── */}
+
+ Variants
+
+ Default
+ Secondary
+ Outline
+ Ghost
+ Destructive
+ Link
+
+ Sizes
+
+ Extra Small
+ Small
+ Default
+ Large
+
+
+
+
+
+
+
+
+
+
+
+
+
+ States
+
+ Disabled
+
+ With Icon
+
+
+
+
+
+
+ {/* ── Badge ──────────────────────────────────────────────── */}
+
+
+ Default
+ Secondary
+ Destructive
+ Success
+ Caution
+ Outline
+
+
+
+
+
+ {/* ── Card ───────────────────────────────────────────────── */}
+
+
+
+
+ Card Title
+ Card description with supporting text.
+
+
+ Body content goes here.
+
+
+ Action
+
+
+
+
+
+ Error Card
+ Something went wrong.
+
+
+ Please check your configuration.
+
+
+
+
+ Clickable variant
+
+
toast("Card clicked", { description: "You clicked the first card." })}>
+
+ Clickable Card
+ Click the entire surface.
+
+
+ Hover, focus and press states built in.
+
+
+
+
toast("Navigation card", { description: "This could navigate somewhere." })}>
+
+ Navigation
+ Tab to me and press Enter.
+
+
+ Keyboard accessible via native button semantics.
+
+
+
+
+
+ Disabled
+ This card is not interactive.
+
+
+ Pointer events and opacity are reduced.
+
+
+
+
+
+
+
+ {/* ── Input ──────────────────────────────────────────────── */}
+
+
+
+
+ {/* ── Separator ──────────────────────────────────────────── */}
+
+
+
Horizontal (default)
+
+
Vertical
+
+ Left
+
+ Right
+
+
+
+
+
+
+ {/* ── Skeleton ───────────────────────────────────────────── */}
+
+
+
+
+ {/* ── Tooltip ────────────────────────────────────────────── */}
+
+
+
+
+ Hover me
+
+
+ Tooltip content
+
+
+
+
+
+
+
+
+
+ Info tooltip (right)
+
+
+
+
+
+
+
+ {/* ── Accordion ──────────────────────────────────────────── */}
+
+
+
+ Is it accessible?
+ Yes. It adheres to the WAI-ARIA design pattern.
+
+
+ Is it styled?
+ Yes. It comes with default styles using Tailwind.
+
+
+ Is it animated?
+ Yes. Transitions are built in.
+
+
+
+
+
+
+ {/* ── Alert ──────────────────────────────────────────────── */}
+
+
+
+
+ Default Alert
+ This is an informational alert message.
+
+
+
+ Destructive Alert
+ Something went wrong. Please try again.
+
+
+
+
+
+
+ {/* ── Alert Dialog ───────────────────────────────────────── */}
+
+
+
+ Delete Account
+
+
+
+ Are you absolutely sure?
+
+ This action cannot be undone. This will permanently delete your account and remove your data from our
+ servers.
+
+
+
+ Cancel
+ Continue
+
+
+
+
+
+
+
+ {/* ── Carousel ───────────────────────────────────────────── */}
+
+
+
+ {Array.from({ length: 5 }).map((_, index) => (
+
+
+
+ {index + 1}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {/* ── Checkbox ───────────────────────────────────────────── */}
+
+
+
+ setCheckboxA(!!v)} />
+ Accept terms and conditions (checked)
+
+
+ setCheckboxB(!!v)} />
+ Subscribe to newsletter (unchecked)
+
+
+
+
+ Disabled checkbox
+
+
+
+
+
+
+
+ {/* ── Collapsible ────────────────────────────────────────── */}
+
+
+
+ @dappnode/toolkit has 3 repositories
+
+
+
+ Toggle
+
+
+
+
+ @dappnode/types
+
+
+
+ @dappnode/common
+
+
+ @dappnode/toolkit
+
+
+
+
+
+
+
+ {/* ── Context Menu ───────────────────────────────────────── */}
+
+
+
+ Right click here
+
+
+
+ Back ⌘[
+
+
+ Forward ⌘]
+
+
+ Reload ⌘R
+
+
+ More Tools
+
+
+ Save Page As… ⇧⌘S
+
+ Create Shortcut…
+ Name Window…
+
+ Developer Tools
+
+
+
+
+ Show Bookmarks Bar ⇧⌘B
+
+ Show Full URLs
+
+ People
+
+ Pedro Duarte
+ Colm Tuite
+
+
+
+
+
+
+
+ {/* ── Dialog ─────────────────────────────────────────────── */}
+
+
+
+ Open Dialog
+
+
+
+ Edit profile
+ Make changes to your profile here. Click save when you're done.
+
+
+
+
+ Cancel
+
+ Save changes
+
+
+
+
+
+
+
+ {/* ── Drawer ─────────────────────────────────────────────── */}
+
+
+
+ Open Drawer
+
+
+
+ Move Goal
+ Set your daily activity goal.
+
+
+
+ setProgress((p) => Math.max(0, p - 10))}>
+ -
+
+ {progress}
+ setProgress((p) => Math.min(100, p + 10))}>
+ +
+
+
+
+
+
+ Submit
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+ {/* ── Empty ──────────────────────────────────────────────── */}
+
+
+
+
+
+
+
+
+ No packages found
+ Install a package from the Store to get started.
+
+
+
+
+
+
+
+
+ {/* ── Field ──────────────────────────────────────────────── */}
+
+
+
+
+
+
+ {/* ── Item ───────────────────────────────────────────────── */}
+
+
+ -
+
+
+
+
+ Default Item
+ A simple item with default variant.
+
+
+ -
+
+
+
+
+ Outline Item
+ Item with a border outline.
+
+
+ -
+
+
+
+
+ Muted Item
+ A muted background item.
+
+
+
+
+
+
+
+ {/* ── Pagination ─────────────────────────────────────────── */}
+
+
+
+
+
+
+
+
+ 1
+
+
+
+ 2
+
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* ── Progress ───────────────────────────────────────────── */}
+
+
+
+
+ {/* ── Radio Group ────────────────────────────────────────── */}
+
+
+
+
+ Option A
+
+
+
+ Option B
+
+
+
+ Option C
+
+
+
+
+
+
+ {/* ── Select ─────────────────────────────────────────────── */}
+
+
+
+
+
+
+
+
+ Fruits
+ Apple
+ Banana
+ Cherry
+
+
+ Vegetables
+ Carrot
+ Celery
+
+
+
+
+
+
+
+
+ {/* ── Sheet ──────────────────────────────────────────────── */}
+
+
+ {(["top", "right", "bottom", "left"] as const).map((side) => (
+
+
+ {side}
+
+
+
+ Sheet — {side}
+ This sheet slides in from the {side}.
+
+
+
Sheet content goes here.
+
+
+
+ Close
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* ── Slider ─────────────────────────────────────────────── */}
+
+
+
+
Value: {sliderValue[0]}
+
+
+
+
+
+
+
+
+ {/* ── Spinner ────────────────────────────────────────────── */}
+
+
+
+
+ {/* ── Switch ─────────────────────────────────────────────── */}
+
+
+
+
+ Airplane Mode ({switchOn ? "on" : "off"})
+
+
+
+
+ Disabled
+
+
+
+
+
+
+
+ {/* ── Sonner (Toast trigger) ─────────────────────────────── */}
+
+
+ The {" "} component is in the AI layout. Each toast
+ type has its own color variant using design tokens.
+
+
+ toast.success("Package installed", { description: "DMS v1.2.3 is now running." })}
+ variant="outline"
+ >
+ Success
+
+ toast.error("Installation failed", { description: "Could not reach IPFS gateway." })}
+ variant="outline"
+ >
+ Error
+
+ toast.warning("Disk space low", { description: "Only 2 GB remaining on /dev/sda1." })}
+ variant="outline"
+ >
+ Warning
+
+ toast.info("Update available", { description: "A new version is available." })}
+ variant="outline"
+ >
+ Info
+
+ toast("Default toast", { description: "This is a default notification." })}
+ variant="outline"
+ >
+ Default
+
+
+
+
+ {/* Bottom spacer */}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/AutoUpdatesDialog.tsx b/packages/admin-ui/src/pages-new/ai/installer/AutoUpdatesDialog.tsx
new file mode 100644
index 0000000000..1853ae1d45
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/AutoUpdatesDialog.tsx
@@ -0,0 +1,108 @@
+import React, { useState, useCallback } from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { prettyDnpName } from "utils/format";
+import { autoUpdateIds } from "params";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter
+} from "components/primitives/dialog";
+import { Button } from "components/primitives/button";
+
+const { MY_PACKAGES } = autoUpdateIds;
+
+interface AutoUpdatesDialogProps {
+ /** The dnpName to enable auto-updates for */
+ dnpName: string;
+ /** Whether the dialog is open */
+ open: boolean;
+ /** Called when the dialog should close */
+ onOpenChange: (open: boolean) => void;
+}
+
+/**
+ * Dialog that prompts the user to enable auto-updates for a
+ * freshly installed package. Replaces the legacy `confirm()` +
+ * `withToastNoThrow()` pattern with new primitives.
+ *
+ * The user can:
+ * - Enable auto-updates for **this package only**
+ * - Enable auto-updates for **all packages**
+ * - Dismiss without enabling
+ */
+export function AutoUpdatesDialog({ dnpName, open, onOpenChange }: AutoUpdatesDialogProps) {
+ const [loading, setLoading] = useState(false);
+ const prettyName = prettyDnpName(dnpName);
+
+ const enableAutoUpdates = useCallback(
+ async (forAll: boolean) => {
+ const id = forAll ? MY_PACKAGES : dnpName;
+ const logId = forAll ? "all packages" : prettyName;
+
+ try {
+ setLoading(true);
+ toast.loading(`Enabling auto-updates for ${logId}…`, { id: `auto-update-${dnpName}` });
+ await api.autoUpdateSettingsEdit({ id, enabled: true });
+ toast.success(`Enabled auto-updates for ${logId}`, { id: `auto-update-${dnpName}` });
+ } catch (e) {
+ const message = e instanceof Error ? e.message : "Unknown error";
+ toast.error(`Failed to enable auto-updates for ${logId}`, {
+ id: `auto-update-${dnpName}`,
+ description: message
+ });
+ console.error("Error enabling auto-updates", e);
+ } finally {
+ setLoading(false);
+ onOpenChange(false);
+ }
+ },
+ [dnpName, prettyName, onOpenChange]
+ );
+
+ return (
+
+
+
+ Enable auto-updates
+
+ Do you want to enable auto-updates for {prettyName} so DAppNode installs the latest
+ versions automatically?
+
+
+
+
+ onOpenChange(false)} disabled={loading}>
+ Cancel
+
+ enableAutoUpdates(true)} disabled={loading}>
+ Enable for all packages
+
+ enableAutoUpdates(false)} disabled={loading}>
+ Enable
+
+
+
+
+ );
+}
+
+/**
+ * Checks whether auto-updates should be prompted for a package.
+ * Returns `true` when auto-updates are not yet enabled for the
+ * package **and** not enabled globally for all packages.
+ */
+export async function shouldPromptAutoUpdates(dnpName: string): Promise {
+ try {
+ const { settings } = await api.autoUpdateDataGet();
+ const enabledForAll = settings[MY_PACKAGES]?.enabled;
+ const enabledForPkg = settings[dnpName]?.enabled;
+ return !enabledForAll && !enabledForPkg;
+ } catch (e) {
+ console.error("Error checking auto-update status", e);
+ return false;
+ }
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerDisclaimerStep.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerDisclaimerStep.tsx
new file mode 100644
index 0000000000..84e1a7a2b3
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerDisclaimerStep.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import RenderMarkdown from "components/RenderMarkdown";
+import { Button } from "components/primitives/button";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import { ShieldAlert, CheckCircle, ArrowLeft } from "lucide-react";
+
+interface InstallerDisclaimerStepProps {
+ disclaimers: { name: string; message: string }[];
+ onAccept: () => void;
+ goBack: () => void;
+}
+
+/**
+ * Disclaimer step — shows disclaimers from the package manifest
+ * and/or a default "unverified" disclaimer.
+ */
+export function InstallerDisclaimerStep({ disclaimers, onAccept, goBack }: InstallerDisclaimerStepProps) {
+ return (
+
+
+
Disclaimers
+
+ Please review and accept the following before installing.
+
+
+
+ {disclaimers.length === 0 ? (
+
+
+ No special disclaimers required.
+
+ ) : (
+ disclaimers.map((disclaimer) => (
+
+
+ {disclaimer.name}
+
+
+
+
+ ))
+ )}
+
+
+
+
+ Back
+
+
{disclaimers.length === 0 ? "Continue" : "Accept & Continue"}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerInfoStep.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerInfoStep.tsx
new file mode 100644
index 0000000000..1ea1bc17b9
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerInfoStep.tsx
@@ -0,0 +1,373 @@
+import React, { useState, useEffect } from "react";
+import { RequestedDnp, upstreamVersionToString } from "@dappnode/types";
+import { prettyDnpName, isDnpVerified, shortAuthor } from "utils/format";
+import humanFileSize from "utils/humanFileSize";
+import getRepoSlugFromManifest from "utils/getRepoSlugFromManifest";
+import RenderMarkdown from "components/RenderMarkdown";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { Separator } from "components/primitives/separator";
+import { Switch } from "components/primitives/switch";
+import { Label } from "components/primitives/label";
+import {
+ ShieldCheck,
+ CheckCircle,
+ XCircle,
+ Loader2,
+ HardDrive,
+ User,
+ Tag,
+ ExternalLink,
+ Download,
+ ArrowUpCircle,
+ ChevronDown,
+ ChevronUp,
+ Package
+} from "lucide-react";
+import defaultAvatar from "img/defaultAvatar.png";
+import { InstallerProgressLogs } from "./InstallerProgressLogs";
+import { ProgressLogs } from "types";
+
+interface InstallerInfoStepProps {
+ dnp: RequestedDnp;
+ onInstall: () => void;
+ disableInstallation: boolean;
+ optionsArray: {
+ name: string;
+ checked: boolean;
+ toggle: () => void;
+ }[];
+ progressLogs?: ProgressLogs;
+ isInstalling?: boolean;
+}
+
+/**
+ * Two-column layout:
+ * - Left: Hero header + description/details
+ * - Right: Sticky sidebar with install CTA, metadata, status, options
+ */
+export function InstallerInfoStep({
+ dnp,
+ onInstall,
+ disableInstallation,
+ optionsArray,
+ progressLogs,
+ isInstalling
+}: InstallerInfoStepProps) {
+ const [showSignedStatus, setShowSignedStatus] = useState(false);
+ const [showResolveStatus, setShowResolveStatus] = useState(false);
+
+ const {
+ dnpName,
+ compatible,
+ signedSafe,
+ signedSafeAll,
+ manifest,
+ isUpdated,
+ isInstalled,
+ avatarUrl,
+ imageSize,
+ origin
+ } = dnp;
+
+ const {
+ shortDescription,
+ description = "No description",
+ author = "Unknown",
+ version,
+ upstreamVersion,
+ upstream
+ } = manifest;
+
+ const parsedUpstreamVersion = upstreamVersionToString({ upstreamVersion, upstream });
+ const repoSlug = getRepoSlugFromManifest(manifest);
+ const isVerified = !origin && isDnpVerified(dnpName);
+
+ const isCompatible = compatible.isCompatible;
+ const resolvingCompatibility = compatible.resolving;
+ const compatibilityError = compatible.error;
+
+ useEffect(() => {
+ if (!isCompatible) setShowResolveStatus(true);
+ }, [isCompatible]);
+ useEffect(() => {
+ if (!signedSafeAll) setShowSignedStatus(true);
+ }, [signedSafeAll]);
+
+ const InstallIcon = isUpdated ? CheckCircle : isInstalled ? ArrowUpCircle : Download;
+ const installLabel = isUpdated ? "Up to date" : isInstalled ? "Update" : "Install";
+
+ return (
+
+ {/* ═══════ Hero header ═══════════════════════════════════════ */}
+
+
+
+
+
+ {prettyDnpName(dnpName)}
+
+ {isVerified && (
+
+
+ Verified
+
+ )}
+
+
{shortAuthor(author)}
+
+ {/* Status pills – inline with the header on desktop */}
+
+ setShowSignedStatus((x) => !x)}
+ expanded={showSignedStatus}
+ />
+ setShowResolveStatus((x) => !x)}
+ expanded={showResolveStatus}
+ />
+
+
+
+
+
+ {/* ═══════ Progress / Installing indicator ═══════════════════ */}
+ {(progressLogs || isInstalling) && (
+
+ )}
+
+ {/* ═══════ Two-column body ═══════════════════════════════════ */}
+
+ {/* ── Main content column ──────────────────────────────────── */}
+
+ {/* Signed status expand */}
+ {showSignedStatus && (
+
+
+
+
+ Signature status
+
+
+ {Object.entries(signedSafe).map(([name, { safe, message }]) => (
+
+ {safe ? (
+
+ ) : (
+
+ )}
+ {prettyDnpName(name)}
+ {message}
+
+ ))}
+
+
+
+ )}
+
+ {/* Compatibility expand */}
+ {showResolveStatus && (
+
+
+
+
+ Compatibility details
+
+ {resolvingCompatibility ? (
+
+
+ Resolving dependencies…
+
+ ) : compatibilityError ? (
+
+
+ Not compatible: {compatibilityError}
+
+ ) : compatible.dnps ? (
+
+
+
+ Package is compatible
+
+ {Object.entries(compatible.dnps).map(([name, { from, to }]) => (
+
+ {prettyDnpName(name)}: {from || "new"} → {to}
+
+ ))}
+
+ ) : null}
+
+
+ )}
+
+ {/* Description / Details */}
+
+
+ {shortDescription && (
+
+ )}
+
+
+ {shortDescription ? "Details" : "About"}
+
+
+
+
+
+
+
+
+ {/* Advanced options */}
+ {optionsArray.length > 0 && (
+
+
+ Advanced options
+
+
+ {optionsArray.map(({ name, checked, toggle }) => (
+
+
+
+ {name}
+
+
+ ))}
+
+
+
+ )}
+
+
+ {/* ── Sidebar column (sticky) ──────────────────────────────── */}
+
+
+
+ {/* Install CTA */}
+
+
+ {installLabel}
+
+
+
+
+ {/* Metadata */}
+
+ } label="Version">
+ {version}
+ {parsedUpstreamVersion && (
+ ({parsedUpstreamVersion})
+ )}
+ {origin && {origin} }
+
+
+ } label="Size">
+ {humanFileSize(imageSize)}
+
+
+ } label="Author">
+
+
+
+
+ {/* GitHub link */}
+ {repoSlug && version && (
+ <>
+
+
+
+ View on GitHub
+
+ >
+ )}
+
+
+
+
+
+ );
+}
+
+/* ── Helper: status pill ────────────────────────────────────────────── */
+
+function StatusPill({
+ ok,
+ label,
+ loading,
+ onClick,
+ expanded
+}: {
+ ok: boolean;
+ label: string;
+ loading: boolean;
+ onClick?: () => void;
+ expanded?: boolean;
+}) {
+ const ExpandIcon = expanded ? ChevronUp : ChevronDown;
+ return (
+
+ {loading ? (
+
+ ) : ok ? (
+
+ ) : (
+
+ )}
+ {label}
+ {onClick && }
+
+ );
+}
+
+/* ── Helper: metadata row ───────────────────────────────────────────── */
+
+function MetaRow({ icon, label, children }: { icon: React.ReactNode; label: string; children: React.ReactNode }) {
+ return (
+
+
+ {icon}
+ {label}
+
+
{children}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerNotificationsStep.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerNotificationsStep.tsx
new file mode 100644
index 0000000000..e245bdb7b6
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerNotificationsStep.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import { CustomEndpoint, GatusEndpoint } from "@dappnode/types";
+import { InstallerEndpointsList } from "pages/notifications/tabs/Settings/InstallerEndpointsList";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { ArrowLeft } from "lucide-react";
+
+interface InstallerNotificationsStepProps {
+ endpointsGatus: GatusEndpoint[];
+ setEndpointsGatus: React.Dispatch>;
+ endpointsCustom: CustomEndpoint[];
+ setEndpointsCustom: React.Dispatch>;
+ goNext: () => void;
+ goBack: () => void;
+}
+
+/**
+ * Notifications step — allows the user to configure notification
+ * endpoints for the package being installed.
+ *
+ * Reuses the legacy `InstallerEndpointsList` component since the
+ * notification endpoint editor is complex and not yet migrated.
+ */
+export function InstallerNotificationsStep({
+ endpointsGatus,
+ setEndpointsGatus,
+ endpointsCustom,
+ setEndpointsCustom,
+ goNext,
+ goBack
+}: InstallerNotificationsStepProps) {
+ return (
+
+
+
Notifications
+
+ Configure notification endpoints for this package.
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+
Accept & Continue
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerPage.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerPage.tsx
new file mode 100644
index 0000000000..67a08832f8
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerPage.tsx
@@ -0,0 +1,68 @@
+import React from "react";
+import { useApi } from "api";
+import { useSelector } from "react-redux";
+import { useParams } from "react-router-dom";
+import { getProgressLogsByDnp } from "services/isInstallingLogs/selectors";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import { Skeleton } from "components/primitives/skeleton";
+import { Spinner } from "components/primitives/spinner";
+import { PageContainer } from "components/primitives/page";
+import { TriangleAlert } from "lucide-react";
+import { InstallerView } from "./InstallerView";
+
+/**
+ * Container component for the new AI installer page.
+ *
+ * Reads the `:id` route param, fetches the `RequestedDnp` via the
+ * `useApi.fetchDnpRequest` hook and passes it to `InstallerView`.
+ */
+export function InstallerPage() {
+ const { id } = useParams<{ id: string }>();
+ const progressLogsByDnp = useSelector(getProgressLogsByDnp);
+
+ if (!id) {
+ return (
+
+
+
+ Missing package ID
+ No package ID was provided in the URL.
+
+
+ );
+ }
+
+ const { data: dnp, error } = useApi.fetchDnpRequest({ id });
+
+ const dnpName = dnp?.dnpName;
+ const progressLogs = dnpName ? progressLogsByDnp[dnpName] : undefined;
+
+ if (error) {
+ return (
+
+
+
+ Failed to load package
+ {typeof error === "string" ? error : error.message}
+
+
+ );
+ }
+
+ if (!dnp) {
+ return (
+
+
+
+
Loading package…
+
+
+
+
+
+
+ );
+ }
+
+ return ;
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerPermissionsStep.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerPermissionsStep.tsx
new file mode 100644
index 0000000000..d15c313dd4
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerPermissionsStep.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+import { SpecialPermissionAllDnps } from "@dappnode/types";
+import { prettyDnpName } from "utils/format";
+import RenderMarkdown from "components/RenderMarkdown";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { CheckCircle, ShieldAlert, ArrowLeft } from "lucide-react";
+
+interface InstallerPermissionsStepProps {
+ permissions: SpecialPermissionAllDnps;
+ onAccept: () => void;
+ goBack: () => void;
+}
+
+/**
+ * Permissions step — lists special permissions requested by each
+ * package in the dependency tree.
+ */
+export function InstallerPermissionsStep({ permissions, onAccept, goBack }: InstallerPermissionsStepProps) {
+ return (
+
+
+
Special Permissions
+
+ Review the special permissions required by this package.
+
+
+
+ {Object.entries(permissions).map(([dnpName, permissionsDnp]) => (
+
+
+
+
+ {prettyDnpName(dnpName)}
+
+
+ {permissionsDnp.length === 0 ? (
+
+
+ Requires no special permissions
+
+ ) : (
+
+ {permissionsDnp.map(({ name, details }) => (
+
+ ))}
+
+ )}
+
+
+ ))}
+
+ {/* Navigation buttons */}
+
+
+
+ Back
+
+
Accept & Continue
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerProgressLogs.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerProgressLogs.tsx
new file mode 100644
index 0000000000..95c86e8270
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerProgressLogs.tsx
@@ -0,0 +1,73 @@
+import React from "react";
+import { isEmpty } from "lodash-es";
+import { prettyDnpName } from "utils/format";
+import { Progress } from "components/primitives/progress";
+import { Card, CardContent } from "components/primitives/card";
+import { Spinner } from "components/primitives/spinner";
+import { ProgressLogs } from "types";
+
+/**
+ * Parse a percentage value from a progress log string like "Downloading 64%".
+ */
+function parsePercent(s: string): number | null {
+ const match = (s || "").match(/\s(\d+?)%/);
+ return match ? parseInt(match[1], 10) : null;
+}
+
+interface InstallerProgressLogsProps {
+ progressLogs?: ProgressLogs;
+ isInstalling?: boolean;
+}
+
+/**
+ * Displays live installation progress for each package being installed.
+ * Shown inline below the package hero header.
+ */
+export function InstallerProgressLogs({ progressLogs, isInstalling }: InstallerProgressLogsProps) {
+ const hasLogs = progressLogs && !isEmpty(progressLogs);
+ const entries = hasLogs ? Object.entries(progressLogs).filter(([name]) => name !== "core.dnp.dappnode.eth") : [];
+
+ // Show spinner when installing but no logs have arrived yet
+ if (!hasLogs && isInstalling) {
+ return (
+
+
+
+ Preparing installation…
+
+
+ );
+ }
+
+ if (!hasLogs) return null;
+
+ return (
+
+
+
+
+
+ Installing{entries.length > 1 ? ` (${entries.length} packages)` : ""}…
+
+
+
+ {entries.map(([dnpName, log = ""]) => {
+ const percent = parsePercent(log);
+ const displayPercent = percent ?? 100;
+
+ return (
+
+
+ {prettyDnpName(dnpName)}
+
+ {percent !== null ? `${percent}%` : log}
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerSetupWizard.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerSetupWizard.tsx
new file mode 100644
index 0000000000..d66f88819c
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerSetupWizard.tsx
@@ -0,0 +1,386 @@
+import React, { useState, useEffect, useCallback } from "react";
+import deepmerge from "deepmerge";
+import { isEmpty } from "lodash-es";
+import ReactMarkdown from "react-markdown";
+import { SetupWizardField, UserSettingsAllDnps, SetupWizardAllDnps } from "@dappnode/types";
+import { prettyDnpName } from "utils/format";
+import { isSecret } from "utils/isSecret";
+import {
+ formDataToUserSettings,
+ userSettingsToFormData,
+ setupWizardToSetupTarget,
+ filterActiveSetupWizard,
+ isSetupWizardEmpty
+} from "pages/installer/parsers/formDataParser";
+import { parseSetupWizardErrors, SetupWizardError } from "pages/installer/parsers/formDataErrors";
+import { SetupWizardFormDataReturn } from "pages/installer/types";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "components/primitives/select";
+import { Switch } from "components/primitives/switch";
+import { Card, CardContent } from "components/primitives/card";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import { ArrowLeft, Eye, EyeOff, Upload, TriangleAlert } from "lucide-react";
+
+/* ── Sub-components ─────────────────────────────────────────────────── */
+function SetupField({
+ field,
+ value,
+ onValueChange,
+ error
+}: {
+ field: SetupWizardField;
+ value: string;
+ onValueChange: (newValue: string) => void;
+ error?: SetupWizardError;
+}) {
+ const [showSecret, setShowSecret] = useState(false);
+
+ const isSecretField =
+ field.secret !== undefined
+ ? field.secret
+ : isSecret(field.id) ||
+ Boolean(field.target && field.target.type === "environment" && isSecret(field.target.name));
+
+ const isFileUpload = field.target?.type === "fileUpload";
+ const isEnum = Boolean(field.enum);
+
+ return (
+
+
+ {field.title}
+ {field.required && * }
+
+
+ {field.description && (
+
+ {field.description}
+
+ )}
+
+ {isEnum ? (
+
+
+
+
+
+ {field.enum!.map((option) => (
+
+ {option}
+
+ ))}
+
+
+ ) : isFileUpload ? (
+
+ ) : isSecretField ? (
+
+ onValueChange(e.target.value)}
+ placeholder={field.title}
+ className="tw:pr-10"
+ />
+ setShowSecret((x) => !x)}
+ className="tw:absolute tw:right-2 tw:top-1/2 tw:-translate-y-1/2 tw:bg-transparent tw:text-muted-foreground tw:hover:text-foreground"
+ >
+ {showSecret ? : }
+
+
+ ) : (
+
onValueChange(e.target.value)}
+ placeholder={field.title}
+ aria-invalid={!!error}
+ />
+ )}
+
+ {error &&
{error.message}
}
+
+ );
+}
+
+/** File upload field with drag-and-drop visual hint */
+function FileUploadField({ value, onValueChange }: { value: string; onValueChange: (v: string) => void }) {
+ const [processing, setProcessing] = useState(false);
+
+ async function onSelectFile(files: FileList) {
+ try {
+ setProcessing(true);
+ const file = files[0];
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ if (event.target?.result) {
+ const dataURL = event.target.result
+ .toString()
+ .replace(";base64", `;name=${encodeURIComponent(file.name)};base64`);
+ onValueChange(dataURL);
+ }
+ };
+ reader.readAsDataURL(file);
+ } catch (e) {
+ console.error("Error processing file:", e);
+ } finally {
+ setProcessing(false);
+ }
+ }
+
+ const fileName = value ? decodeFileName(value) : null;
+
+ return (
+
+
+
+ {processing ? "Loading file…" : fileName ?? "Choose file"}
+
+ e.target.files && onSelectFile(e.target.files)} />
+
+ );
+}
+
+/** Extract the file name from a data URL */
+function decodeFileName(dataURL: string): string | null {
+ const match = dataURL.match(/;name=([^;]+);/);
+ if (match) {
+ try {
+ return decodeURIComponent(match[1]);
+ } catch {
+ return match[1];
+ }
+ }
+ return null;
+}
+
+/* ── Advanced editor ────────────────────────────────────────────────── */
+
+function AdvancedEditor({
+ userSettings,
+ onChange
+}: {
+ userSettings: UserSettingsAllDnps;
+ onChange: (newUserSettings: UserSettingsAllDnps) => void;
+}) {
+ return (
+
+ {Object.entries(userSettings).map(([dnpName, dnpSettings]) => (
+
+
{prettyDnpName(dnpName)}
+
+ {/* Environment variables */}
+ {dnpSettings.environment &&
+ Object.entries(dnpSettings.environment).map(([serviceName, environment]) => (
+
+ ))}
+
+ {/* Port mappings */}
+ {dnpSettings.portMappings &&
+ Object.entries(dnpSettings.portMappings).map(([serviceName, portMappings]) => (
+
+ ))}
+
+ {/* Named volume mountpoints */}
+ {dnpSettings.namedVolumeMountpoints && !isEmpty(dnpSettings.namedVolumeMountpoints) && (
+
+ )}
+
+ ))}
+
+ );
+}
+
+/* ── Main component ─────────────────────────────────────────────────── */
+
+interface InstallerSetupWizardProps {
+ setupWizard: SetupWizardAllDnps;
+ userSettings: UserSettingsAllDnps;
+ onSubmit: (newUserSettings: UserSettingsAllDnps) => void;
+ goBack: () => void;
+}
+
+export function InstallerSetupWizard({
+ setupWizard,
+ userSettings: initialUserSettings,
+ onSubmit,
+ goBack
+}: InstallerSetupWizardProps) {
+ const isWizardEmpty = isSetupWizardEmpty(setupWizard);
+ const [showAdvanced, setShowAdvanced] = useState(isWizardEmpty);
+ const [submitting, setSubmitting] = useState(false);
+ const [userSettings, setUserSettings] = useState(initialUserSettings);
+
+ useEffect(() => {
+ setUserSettings(initialUserSettings);
+ }, [initialUserSettings]);
+
+ useEffect(() => {
+ setShowAdvanced(isWizardEmpty);
+ }, [isWizardEmpty]);
+
+ // Derived data for the visual wizard
+ const setupTarget = setupWizardToSetupTarget(setupWizard);
+ const formData = userSettingsToFormData(userSettings, setupTarget);
+ const setupWizardActive = filterActiveSetupWizard(setupWizard, formData);
+ const dataErrors = parseSetupWizardErrors(setupWizardActive, formData);
+ const visibleDataErrors = dataErrors.filter((error) => submitting || error.type !== "empty");
+
+ const onNewUserSettings = useCallback((newUserSettings: UserSettingsAllDnps) => {
+ setSubmitting(false);
+ setUserSettings((prev) => deepmerge(prev, newUserSettings));
+ }, []);
+
+ function onNewFormData(newFormData: SetupWizardFormDataReturn) {
+ onNewUserSettings(formDataToUserSettings(newFormData, setupTarget));
+ }
+
+ function handleSubmit() {
+ if (dataErrors.length) {
+ setSubmitting(true);
+ } else {
+ onSubmit(userSettings);
+ }
+ }
+
+ return (
+
+ {/* Header */}
+
+
Configuration
+
+ {showAdvanced
+ ? "Edit raw environment variables, ports & volumes."
+ : "Configure the package before installing."}
+
+
+
+ {/* Editor card */}
+
+
+ {showAdvanced ? (
+
+ ) : (
+ /* Visual wizard */
+
+ {Object.entries(setupWizardActive).map(([dnpName, setupWizardDnp]) => (
+
+
{prettyDnpName(dnpName)}
+ {setupWizardDnp.fields.map((field) => {
+ const fieldError = visibleDataErrors.find((e) => e.dnpName === dnpName && e.id === field.id);
+ return (
+ onNewFormData({ [dnpName]: { [field.id]: newValue } })}
+ error={fieldError}
+ />
+ );
+ })}
+
+ ))}
+
+ )}
+
+ {/* Validation errors summary */}
+ {visibleDataErrors.length > 0 && (
+
+
+
+ {visibleDataErrors.map(({ dnpName, id, title, type, message }) => (
+
+ {prettyDnpName(dnpName)} — {title}: {message}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Bottom bar */}
+
+
+
+
+ Back
+
+
+ {/* Advanced toggle — only show if the wizard is not empty */}
+ {!isWizardEmpty && (
+
+
+
+ Advanced editor
+
+
+ )}
+
+
+
Submit & Continue
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerStepper.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerStepper.tsx
new file mode 100644
index 0000000000..633321c706
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerStepper.tsx
@@ -0,0 +1,56 @@
+import React from "react";
+import { cn } from "lib/utils";
+import { Check } from "lucide-react";
+
+interface InstallerStepperProps {
+ steps: string[];
+ currentIndex: number;
+}
+
+export function InstallerStepper({ steps, currentIndex }: InstallerStepperProps) {
+ return (
+
+ {/* Segmented pill container */}
+
+ {steps.map((step, i) => {
+ const isCompleted = i < currentIndex;
+ const isActive = i === currentIndex;
+
+ return (
+
+
+ {isCompleted ? : i + 1}
+
+ {step}
+
+ );
+ })}
+
+
+ {/* Overall progress bar */}
+
+
1 ? (currentIndex / (steps.length - 1)) * 100 : 0}%`
+ }}
+ />
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerView.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerView.tsx
new file mode 100644
index 0000000000..cc0e3834b4
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerView.tsx
@@ -0,0 +1,448 @@
+import React, { useState, useEffect, useRef } from "react";
+import { api, useApi } from "api";
+import { useDispatch } from "react-redux";
+import { Routes, Route, useNavigate, useLocation, useParams } from "react-router-dom";
+import { isEmpty, throttle } from "lodash-es";
+import { difference } from "utils/lodashExtended";
+import { prettyDnpName, isDnpVerified } from "utils/format";
+import { diff } from "semver";
+import { toast } from "sonner";
+// Parsers & helpers
+import { isSetupWizardEmpty } from "pages/installer/parsers/formDataParser";
+import { clearIsInstallingLog } from "services/isInstallingLogs/actions";
+import { continueIfCalleDisconnected } from "api/utils";
+// Types
+import { ProgressLogs } from "types";
+import { RequestedDnp, UserSettingsAllDnps, CustomEndpoint, GatusEndpoint } from "@dappnode/types";
+// New components
+import { Button } from "components/primitives/button";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import { TriangleAlert, ExternalLink } from "lucide-react";
+import { InstallerStepper } from "./InstallerStepper";
+import { InstallerInfoStep } from "./InstallerInfoStep";
+import { InstallerPermissionsStep } from "./InstallerPermissionsStep";
+import { InstallerWarningsStep } from "./InstallerWarningsStep";
+import { InstallerDisclaimerStep } from "./InstallerDisclaimerStep";
+import { InstallerNotificationsStep } from "./InstallerNotificationsStep";
+import { InstallerSetupWizard } from "./InstallerSetupWizard";
+import { AutoUpdatesDialog, shouldPromptAutoUpdates } from "./AutoUpdatesDialog";
+import { pathName as systemPathName, subPaths as systemSubPaths } from "pages/system/data";
+import { withLegacyBase } from "utils/path";
+import { packagesRelativePath } from "../packages/data";
+import { PageContainer } from "components/primitives/page";
+
+interface InstallerViewProps {
+ dnp: RequestedDnp;
+ progressLogs?: ProgressLogs;
+}
+
+/**
+ * Main installer view — orchestrates the multi-step install flow
+ * using the new design system. Mirrors the logic of the legacy
+ * `InstallDnpView` but with shadcn/ui primitives.
+ */
+export function InstallerView({ dnp, progressLogs }: InstallerViewProps) {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const params = useParams();
+ const dispatch = useDispatch();
+
+ const [userSettings, setUserSettings] = useState
({});
+ const [bypassCoreOpt, setBypassCoreOpt] = useState();
+ const [bypassSignedOpt, setBypassSignedOpt] = useState();
+ const [showAdvancedEditor, setShowAdvancedEditor] = useState(false);
+ const [isInstalling, setIsInstalling] = useState(false);
+ /** Tracks whether we've seen progress logs at least once during this install */
+ const hasSeenLogs = useRef(false);
+ const [notificationsPkgInstalled, setNotificationsPkgInstalled] = useState(false);
+ /** Controls the auto-updates dialog shown after install completion */
+ const [autoUpdatesDialogOpen, setAutoUpdatesDialogOpen] = useState(false);
+
+ const {
+ dnpName,
+ reqVersion,
+ semVersion,
+ settings,
+ manifest,
+ setupWizard,
+ isInstalled,
+ installedVersion,
+ notificationsSettings
+ } = dnp;
+
+ const updateType = installedVersion && diff(installedVersion, semVersion);
+ const areUpdateWarnings =
+ manifest.warnings?.onPatchUpdate || manifest.warnings?.onMinorUpdate || manifest.warnings?.onMajorUpdate;
+ const isCore = manifest.type === "dncore";
+ const permissions = dnp.specialPermissions;
+ const hasPermissions = Object.values(permissions).some((p) => p.length > 0);
+ const requiresCoreUpdate = dnp.compatible.requiresCoreUpdate;
+ const requiresDockerUpdate = dnp.compatible.requiresDockerUpdate;
+ const packagesToBeUninstalled = dnp.compatible.packagesToBeUninstalled;
+ const isWizardEmpty = isSetupWizardEmpty(setupWizard);
+ const oldEditorAvailable = Boolean(userSettings);
+
+ const [endpoints, setEndpoints] = useState(manifest.notifications?.endpoints || []);
+ const [customEndpoints, setCustomEndpoints] = useState(
+ manifest.notifications?.customEndpoints || []
+ );
+
+ const notificationsPkgStatusRequest = useApi.notificationsPackageStatus();
+
+ useEffect(() => {
+ if (notificationsPkgStatusRequest.data) {
+ setNotificationsPkgInstalled(notificationsPkgStatusRequest.data.isInstalled);
+ }
+ }, [notificationsPkgStatusRequest.data]);
+
+ useEffect(() => {
+ if (notificationsSettings && notificationsSettings[dnpName]) {
+ setEndpoints(notificationsSettings[dnpName].endpoints || []);
+ setCustomEndpoints(notificationsSettings[dnpName].customEndpoints || []);
+ }
+ }, [notificationsSettings, dnpName]);
+
+ useEffect(() => {
+ setUserSettings(settings || {});
+ }, [settings]);
+
+ const componentIsMounted = useRef(true);
+ useEffect(() => {
+ return () => {
+ componentIsMounted.current = false;
+ };
+ }, []);
+
+ /* ── Install handler ──────────────────────────────────────────── */
+
+ const onInstall = async (newData?: { newUserSettings: UserSettingsAllDnps }) => {
+ const _userSettings = newData && newData.newUserSettings ? newData.newUserSettings : userSettings;
+ const prettyName = prettyDnpName(dnpName);
+
+ try {
+ setIsInstalling(true);
+ hasSeenLogs.current = false;
+ toast.loading(`Installing ${prettyName}…`, { id: dnpName });
+
+ // continueIfCalleDisconnected returns a function — invoke it with ()
+ await continueIfCalleDisconnected(
+ () =>
+ api.packageInstall({
+ name: dnpName,
+ version: reqVersion,
+ userSettings: difference(settings || {}, _userSettings),
+ options: {
+ BYPASS_CORE_RESTRICTION: bypassCoreOpt,
+ BYPASS_SIGNED_RESTRICTION: bypassSignedOpt
+ },
+ notificationsSettings: {
+ [dnpName]: {
+ endpoints: endpoints.length > 0 ? endpoints : undefined,
+ customEndpoints: customEndpoints.length > 0 ? customEndpoints : undefined
+ }
+ }
+ }),
+ dnpName
+ )();
+
+ // NOTE: Do NOT show success or redirect here.
+ // continueIfCalleDisconnected resolves immediately when the
+ // dappmanager disconnects during install. The actual install is
+ // still in progress — tracked via progressLogs from Redux.
+ // A useEffect below watches for progressLogs to clear, then
+ // fires the success toast and redirects.
+ } catch (e) {
+ const message = e instanceof Error ? e.message : "Unknown error";
+ toast.error(`Failed to install ${prettyName}`, {
+ id: dnpName,
+ description: message
+ });
+ console.error(e);
+ // Only reset on error — success is handled by the effect below
+ dispatch(clearIsInstallingLog({ id: dnpName }));
+ if (componentIsMounted.current) setIsInstalling(false);
+ }
+ };
+
+ /* ── Detect install completion via progressLogs ───────────────── */
+
+ useEffect(() => {
+ if (!isInstalling) return;
+
+ const hasLogs = progressLogs && !isEmpty(progressLogs);
+
+ // Track that we've seen progress logs at least once
+ if (hasLogs) {
+ hasSeenLogs.current = true;
+ }
+
+ // Install is done: we saw logs before, and now they're gone
+ if (hasSeenLogs.current && !hasLogs) {
+ const prettyName = prettyDnpName(dnpName);
+ toast.success(`${prettyName} installed successfully`, { id: dnpName });
+
+ dispatch(clearIsInstallingLog({ id: dnpName }));
+ setIsInstalling(false);
+ hasSeenLogs.current = false;
+
+ // Check if we should prompt the auto-updates dialog.
+ // If yes → open it (redirect happens when the dialog closes).
+ // If no → redirect to the store after a short delay.
+ shouldPromptAutoUpdates(dnpName).then((shouldPrompt) => {
+ if (shouldPrompt && componentIsMounted.current) {
+ setAutoUpdatesDialogOpen(true);
+ } else if (componentIsMounted.current) {
+ navigate(`${packagesRelativePath}/${encodeURIComponent(dnpName)}/info`);
+ }
+ });
+ }
+ }, [progressLogs, isInstalling, dnpName, dispatch, navigate]);
+
+ const onInstallThrottle = throttle(onInstall, 1000);
+
+ /* ── Disclaimers ──────────────────────────────────────────────── */
+
+ const disclaimers: { name: string; message: string }[] = [];
+ if (!isDnpVerified(dnpName) || dnp.origin)
+ disclaimers.push({
+ name: "Unverified package",
+ message:
+ "This package has been developed by a third party. DAppNode association is not maintaining this package and has not performed any audit on its content. Use it at your own risk. DAppNode will not be liable for any loss or damage produced by the use of this package"
+ });
+ if (manifest.disclaimer) disclaimers.push({ name: prettyDnpName(dnpName), message: manifest.disclaimer.message });
+
+ /* ── Advanced options ─────────────────────────────────────────── */
+
+ const optionsArray = [
+ {
+ name: "Show advanced editor",
+ available: isWizardEmpty && oldEditorAvailable,
+ checked: showAdvancedEditor,
+ toggle: () => setShowAdvancedEditor((x) => !x)
+ },
+ {
+ name: "Bypass core restriction",
+ available: dnp.origin && isCore,
+ checked: bypassCoreOpt ?? false,
+ toggle: () => setBypassCoreOpt((x) => !x)
+ },
+ {
+ name: "Bypass only signed safe restriction",
+ available: !dnp.signedSafeAll,
+ checked: bypassSignedOpt ?? false,
+ toggle: () => setBypassSignedOpt((x) => !x)
+ }
+ ].filter((option) => option.available);
+
+ const disableInstallation =
+ !isEmpty(progressLogs) || requiresCoreUpdate || requiresDockerUpdate || packagesToBeUninstalled.length > 0;
+
+ /* ── Step routes ──────────────────────────────────────────────── */
+
+ const setupSubPath = "setup";
+ const permissionsSubPath = "permissions";
+ const warningsSubPath = "warnings";
+ const disclaimerSubPath = "disclaimer";
+ const notificationsSubPath = "notifications";
+ const installSubPath = "install";
+
+ const showNotificationsStep = notificationsPkgInstalled && manifest.notifications;
+
+ const availableRoutes = [
+ {
+ name: "Setup",
+ subPath: setupSubPath,
+ render: () => (
+ {
+ setUserSettings(newUserSettings);
+ goNext({ newUserSettings });
+ }}
+ goBack={goBack}
+ />
+ ),
+ available: !isWizardEmpty || showAdvancedEditor
+ },
+ {
+ name: "Permissions",
+ subPath: permissionsSubPath,
+ render: () => ,
+ available: hasPermissions
+ },
+ {
+ name: "Warnings",
+ subPath: warningsSubPath,
+ render: () => (
+
+ ),
+ available: manifest.warnings?.onInstall || (areUpdateWarnings && isInstalled && updateType)
+ },
+ {
+ name: "Disclaimer",
+ subPath: disclaimerSubPath,
+ render: () => ,
+ available: disclaimers.length > 0
+ },
+ {
+ name: "Notifications",
+ subPath: notificationsSubPath,
+ render: () => (
+
+ ),
+ available: showNotificationsStep
+ },
+ {
+ name: "Install",
+ subPath: installSubPath,
+ available: true
+ }
+ ].filter((route) => route.available);
+
+ const currentSubRoute = location.pathname.split(`${encodeURIComponent(params.id || "")}/`)[1] || "";
+ const currentIndex = availableRoutes.findIndex(({ subPath }) => subPath && currentSubRoute.includes(subPath));
+
+ const requiredDockerVersion = manifest.requirements?.minimumDockerVersion;
+
+ /* ── Redirect on mount if sub-route present ───────────────────── */
+ useEffect(() => {
+ if (currentSubRoute) navigate(".");
+ }, []);
+
+ /* ── Navigation ───────────────────────────────────────────────── */
+
+ function goNext(newData?: { newUserSettings: UserSettingsAllDnps }) {
+ const nextIndex = currentIndex + 1;
+ if (nextIndex >= availableRoutes.length - 1) {
+ navigate(".");
+ onInstallThrottle(newData);
+ } else {
+ const nextStep = availableRoutes[nextIndex];
+ if (nextStep) navigate(nextStep.subPath);
+ }
+ }
+
+ function goBack() {
+ if (currentIndex <= 0) {
+ // At the first step or info page — go back to info page (index route)
+ navigate(".");
+ } else {
+ const prevStep = availableRoutes[currentIndex - 1];
+ // Navigate to the previous step's subPath relative to the installer base
+ navigate(`../${prevStep.subPath}`);
+ }
+ }
+
+ /* ── Render ───────────────────────────────────────────────────── */
+
+ return (
+
+ {/* ── Blocking alerts ──────────────────────────────────────── */}
+ {(requiresCoreUpdate || requiresDockerUpdate || packagesToBeUninstalled.length > 0) && (
+
+ {requiresCoreUpdate && (
+
+
+ Core update required
+
+ {prettyDnpName(dnpName)} requires a more recent version of DAppNode. Update your
+ DAppNode before continuing.
+
+
+ )}
+
+ {requiresDockerUpdate && (
+
+
+ Docker update required
+
+
+ {prettyDnpName(dnpName)} requires at least Docker{" "}
+ {requiredDockerVersion} . Update Docker in System → Advanced .
+
+ navigate(withLegacyBase(`${systemPathName}/${systemSubPaths.advanced}`))}
+ >
+ Go to Advanced
+
+
+
+
+ )}
+
+ {packagesToBeUninstalled.length > 0 && (
+
+
+ Packages must be uninstalled first
+
+ {prettyDnpName(dnpName)} requires uninstalling:{" "}
+ {packagesToBeUninstalled.map((pkg, i) => (
+
+ {prettyDnpName(pkg)}
+ {i < packagesToBeUninstalled.length - 1 ? ", " : ""}
+
+ ))}
+
+
+ )}
+
+ )}
+
+ {/* ── Stepper (only when in a sub-step) ────────────────────── */}
+ {currentIndex >= 0 && availableRoutes.length > 1 && (
+ r.name)} currentIndex={currentIndex} />
+ )}
+
+ {/* ── Step routes ──────────────────────────────────────────── */}
+
+ goNext()}
+ disableInstallation={disableInstallation}
+ optionsArray={optionsArray}
+ progressLogs={progressLogs}
+ isInstalling={isInstalling}
+ />
+ }
+ />
+ {availableRoutes
+ .filter((route) => route.render)
+ .map((route) => (
+ {route.render!()}>} />
+ ))}
+
+
+ {/* ── Auto-updates dialog (shown after successful install) ── */}
+ {
+ setAutoUpdatesDialogOpen(open);
+ // When the dialog closes, redirect to the store
+ if (!open && componentIsMounted.current) {
+ navigate(`${packagesRelativePath}/${encodeURIComponent(dnpName)}/info`);
+ }
+ }}
+ />
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/InstallerWarningsStep.tsx b/packages/admin-ui/src/pages-new/ai/installer/InstallerWarningsStep.tsx
new file mode 100644
index 0000000000..a45a52df48
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/InstallerWarningsStep.tsx
@@ -0,0 +1,92 @@
+import React from "react";
+import { Manifest, RequestedDnp } from "@dappnode/types";
+import { ReleaseType } from "semver";
+import RenderMarkdown from "components/RenderMarkdown";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import { TriangleAlert, ArrowLeft } from "lucide-react";
+
+interface InstallerWarningsStepProps {
+ goNext: () => void;
+ goBack: () => void;
+ warnings: Manifest["warnings"];
+ isInstalled: RequestedDnp["isInstalled"];
+ updateType?: ReleaseType | null | "";
+}
+
+/**
+ * Warnings step — shows contextual warnings for the current install/update.
+ */
+export function InstallerWarningsStep({
+ goNext,
+ goBack,
+ warnings,
+ isInstalled,
+ updateType
+}: InstallerWarningsStepProps) {
+ if (!warnings) {
+ return (
+
+
+
+ No warnings to display.
+
+
+
+
+
+ Back
+
+
Continue
+
+
+ );
+ }
+
+ let warningTitle = "";
+ let warningContent = "";
+
+ if (isInstalled && warnings.onInstall) {
+ warningTitle = "Installation Warning";
+ warningContent = warnings.onInstall;
+ } else if (updateType === "patch" && warnings.onPatchUpdate) {
+ warningTitle = "Patch Update Warning";
+ warningContent = warnings.onPatchUpdate;
+ } else if (updateType === "minor" && warnings.onMinorUpdate) {
+ warningTitle = "Minor Update Warning";
+ warningContent = warnings.onMinorUpdate;
+ } else if (updateType === "major" && warnings.onMajorUpdate) {
+ warningTitle = "Major Update Warning";
+ warningContent = warnings.onMajorUpdate;
+ }
+
+ return (
+
+
+
Warnings
+
+ Please review the following warnings before continuing.
+
+
+
+ {warningContent && (
+
+
+ {warningTitle}
+
+
+
+
+ )}
+
+
+
+
+ Back
+
+
Continue
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/installer/data.ts b/packages/admin-ui/src/pages-new/ai/installer/data.ts
new file mode 100644
index 0000000000..dca573a49f
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/data.ts
@@ -0,0 +1,3 @@
+const installerRelativePath = "/installer";
+
+export { installerRelativePath };
diff --git a/packages/admin-ui/src/pages-new/ai/installer/index.ts b/packages/admin-ui/src/pages-new/ai/installer/index.ts
new file mode 100644
index 0000000000..0a59198553
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/installer/index.ts
@@ -0,0 +1,9 @@
+export { InstallerPage } from "./InstallerPage";
+export { InstallerView } from "./InstallerView";
+export { InstallerInfoStep } from "./InstallerInfoStep";
+export { InstallerStepper } from "./InstallerStepper";
+export { InstallerPermissionsStep } from "./InstallerPermissionsStep";
+export { InstallerWarningsStep } from "./InstallerWarningsStep";
+export { InstallerDisclaimerStep } from "./InstallerDisclaimerStep";
+export { InstallerNotificationsStep } from "./InstallerNotificationsStep";
+export { InstallerProgressLogs } from "./InstallerProgressLogs";
diff --git a/packages/admin-ui/src/pages-new/ai/nexus/NexusPage.tsx b/packages/admin-ui/src/pages-new/ai/nexus/NexusPage.tsx
new file mode 100644
index 0000000000..07be50fb34
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/nexus/NexusPage.tsx
@@ -0,0 +1,183 @@
+import * as React from "react";
+import { PageContainer } from "components/primitives/page";
+import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { Separator } from "components/primitives/separator";
+import { TypographyH4, TypographyInlineCode, TypographyMuted } from "components/primitives/typography";
+import {
+ ShieldCheck,
+ Wallet,
+ Plug,
+ BarChart3,
+ ExternalLink,
+ Sparkles,
+ UserPlus,
+ CreditCard,
+ KeyRound
+} from "lucide-react";
+import { nexusExternalUrl, nexusLandingPageUrl } from "./data";
+
+/* ── Feature data ───────────────────────────────────────────────────── */
+
+const features = [
+ {
+ icon: ShieldCheck,
+ title: "Private AI Models",
+ description: "All models through one unified API. Switch between private and standard models instantly."
+ },
+ {
+ icon: Wallet,
+ title: "Flexible Pricing",
+ description: "Select between prepaid credits or monthly subscriptions based on your needs."
+ },
+ {
+ icon: Plug,
+ title: "Easy Integration",
+ description: "Manage your API key to integrate the models with your preferred AI agents."
+ },
+ {
+ icon: BarChart3,
+ title: "Real-Time Usage",
+ description: "Monitor how many tokens you're spending, on which models, over time."
+ }
+];
+
+/* ── Getting-started steps ──────────────────────────────────────────── */
+
+const steps = [
+ {
+ icon: UserPlus,
+ title: "Create your account",
+ description: "Sign up at Nexus to start running AI models privately."
+ },
+ {
+ icon: CreditCard,
+ title: "Buy credits or subscribe",
+ description: "Get 5 € in free credits and test the power of private AI models."
+ },
+ {
+ icon: KeyRound,
+ title: "Create your API key",
+ description: "Use your API key to set up models in your favourite AI agent."
+ }
+];
+
+/* ── Page component ─────────────────────────────────────────────────── */
+
+export function NexusPage() {
+ return (
+
+ {/* ── Hero ──────────────────────────────────────────────────── */}
+
+
+
+ Built into every Dappnode AI product
+
+
+
+ The gateway for AI builders{" "}
+
+ who value privacy
+
+
+
+
+ Access top AI models through a single OpenAI-compatible API endpoint — filtered by privacy, speed, and
+ reasoning level.
+
+
+
+ window.open(nexusLandingPageUrl, "_blank")}>
+ Learn more
+
+
+ window.open(nexusExternalUrl, "_blank")}>
+ Open Dashboard
+
+
+
+
+
+
+
+ {/* ── Integration callout ───────────────────────────────────── */}
+
+
+
+
+ Nexus is integrated into every AI product in Dappnode
+
+ If a direct integration isn't available, you can always connect via the API endpoint:{" "}
+ nexus.dappnode.com/v1
+
+
+
+
+
+ {/* ── Features grid ─────────────────────────────────────────── */}
+
+
+ Key Benefits
+
+ Dappnode Nexus provides a curated selection of AI models to run in a few clicks.
+
+
+
+
+ {features.map((f) => (
+
+
+
+
+
+ {f.description}
+
+
+ ))}
+
+
+
+
+
+ {/* ── Getting started ───────────────────────────────────────── */}
+
+
+ How to Get Started
+ Three simple steps to start using Nexus.
+
+
+
+ {steps.map((s, i) => (
+
+
+
+ {i + 1}
+
+ {s.title}
+
+
+ {s.description}
+
+
+ ))}
+
+
+ {/* Bottom CTA */}
+
+ window.open(nexusExternalUrl, "_blank")}>
+ Create your account
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/nexus/data.ts b/packages/admin-ui/src/pages-new/ai/nexus/data.ts
new file mode 100644
index 0000000000..59cb3b5997
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/nexus/data.ts
@@ -0,0 +1,5 @@
+const nexusRelativePath = "/ai/nexus";
+const nexusExternalUrl = "https://nexus.dappnode.com/";
+const nexusDocsUrl = "https://nexus.docs.dappnode.com";
+
+export { nexusRelativePath, nexusExternalUrl, nexusDocsUrl as nexusLandingPageUrl };
diff --git a/packages/admin-ui/src/pages-new/ai/packages/components/ServiceSelector.tsx b/packages/admin-ui/src/pages-new/ai/packages/components/ServiceSelector.tsx
new file mode 100644
index 0000000000..098438b150
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/components/ServiceSelector.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { Label } from "components/primitives/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "components/primitives/select";
+import { PackageContainer } from "@dappnode/types";
+
+export function ServiceSelector({
+ serviceName,
+ setServiceName,
+ containers
+}: {
+ serviceName: string;
+ setServiceName: (name: string) => void;
+ containers: PackageContainer[];
+}) {
+ const serviceNames = containers.map((c) => c.serviceName);
+ if (serviceNames.length <= 1) return null;
+
+ return (
+
+ Service
+
+
+
+
+
+ {serviceNames.map((name) => (
+
+ {name}
+
+ ))}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/data.ts b/packages/admin-ui/src/pages-new/ai/packages/data.ts
new file mode 100644
index 0000000000..5c1714334f
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/data.ts
@@ -0,0 +1,3 @@
+const packagesRelativePath = "/ai/packages";
+
+export { packagesRelativePath };
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/BackupTab.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/BackupTab.tsx
new file mode 100644
index 0000000000..ca4a288eff
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/BackupTab.tsx
@@ -0,0 +1,202 @@
+import React, { useState, useRef } from "react";
+import { api, apiRoutes } from "api";
+import { toast } from "sonner";
+import { PackageBackup } from "@dappnode/types";
+import { prettyDnpName } from "utils/format";
+import humanFileSize from "utils/humanFileSize";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Progress } from "components/primitives/progress";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle
+} from "components/primitives/alert-dialog";
+import { Download, Upload, TriangleAlert, Archive } from "lucide-react";
+
+export function BackupTab({ dnpName, backup }: { dnpName: string; backup: PackageBackup[] }) {
+ if (!Array.isArray(backup) || backup.length === 0) return null;
+
+ return (
+
+
+
+
+ );
+}
+
+/* ── Download backup ────────────────────────────────────────────────── */
+
+function BackupDownloadCard({ dnpName, backup }: { dnpName: string; backup: PackageBackup[] }) {
+ const [loading, setLoading] = useState(false);
+ const [downloadUrl, setDownloadUrl] = useState();
+
+ async function prepareBackup() {
+ const prettyName = prettyDnpName(dnpName);
+ try {
+ setLoading(true);
+ toast.loading(`Preparing backup for ${prettyName}…`, { id: "backup-dl" });
+ const fileId = await api.backupGet({ dnpName, backup });
+ if (!fileId) throw Error("No fileId returned");
+ const url = apiRoutes.downloadUrl({ fileId });
+ window.open(url, "_newtab");
+ setDownloadUrl(url);
+ toast.success(`Backup for ${prettyName} ready`, { id: "backup-dl" });
+ } catch (e) {
+ toast.error(`Failed to prepare backup: ${e}`, { id: "backup-dl" });
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+
+ Download Backup
+
+
+ Download a backup of the critical files of this package to your local machine.
+
+
+
+ {downloadUrl ? (
+ <>
+
+
+
+ Download backup
+
+
+ Allow browser pop-ups or click download.
+
+
+ Sensitive data
+ This backup may contain private keys. Store it safely.
+
+ >
+ ) : (
+
+
+ {loading ? "Preparing…" : "Backup now"}
+
+ )}
+
+ {loading && }
+
+
+ );
+}
+
+/* ── Restore backup ─────────────────────────────────────────────────── */
+
+function BackupRestoreCard({ dnpName, backup }: { dnpName: string; backup: PackageBackup[] }) {
+ const [restoring, setRestoring] = useState(false);
+ const [progress, setProgress] = useState<{ label: string; percent?: number }>();
+ const [selectedFile, setSelectedFile] = useState();
+ const [showConfirm, setShowConfirm] = useState(false);
+ const inputRef = useRef(null);
+
+ async function doRestore(file: File) {
+ const prettyName = prettyDnpName(dnpName);
+ try {
+ setRestoring(true);
+ setProgress({ label: "Uploading file…" });
+
+ const { fileId } = await apiRoutes.uploadFile(file, (ev) => {
+ const percent = parseFloat(((100 * (ev.loaded || 0)) / (ev.total || 1)).toFixed(2));
+ setProgress({ percent, label: `${percent}% ${humanFileSize(ev.loaded)} / ${humanFileSize(ev.total)}` });
+ });
+
+ setProgress({ label: "Restoring backup…" });
+ toast.loading(`Restoring backup for ${prettyName}…`, { id: "backup-restore" });
+ await api.backupRestore({ dnpName, backup, fileId });
+ toast.success(`Restored backup for ${prettyName}`, { id: "backup-restore" });
+ } catch (e) {
+ toast.error(`Restore failed: ${e}`, { id: "backup-restore" });
+ } finally {
+ setRestoring(false);
+ setProgress(undefined);
+ setSelectedFile(undefined);
+ }
+ }
+
+ function handleFileSelect(e: React.ChangeEvent) {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ setSelectedFile(file);
+ setShowConfirm(true);
+ }
+
+ return (
+ <>
+
+
+
+
+ Restore Backup
+
+ Restore from an existing backup. This will overwrite existing data.
+
+
+
+ inputRef.current?.click()}>
+
+ {restoring ? "Restoring…" : "Select backup file"}
+
+
+
+
+ {progress && (
+
+ )}
+
+
+
+ {/* Confirm dialog */}
+
+
+
+ Restore backup
+
+ This action cannot be undone. The backup data will overwrite any existing data.
+ {selectedFile && (
+
+ Selected file: {selectedFile.name} ({humanFileSize(selectedFile.size)})
+
+ )}
+
+
+
+ setSelectedFile(undefined)}>Cancel
+ {
+ if (selectedFile) doRestore(selectedFile);
+ setShowConfirm(false);
+ }}
+ >
+ Restore
+
+
+
+
+ >
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/ConfigTab.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/ConfigTab.tsx
new file mode 100644
index 0000000000..ec434bc8d9
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/ConfigTab.tsx
@@ -0,0 +1,215 @@
+import React, { useState, useEffect, useMemo } from "react";
+import { toast } from "sonner";
+import { api } from "api";
+import { UserSettingsAllDnps, UserSettings, SetupWizard as SetupWizardType, PackageEnvs } from "@dappnode/types";
+import { difference } from "utils/lodashExtended";
+import { prettyDnpName } from "utils/format";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "components/primitives/select";
+import { Switch } from "components/primitives/switch";
+import { Separator } from "components/primitives/separator";
+import { Settings, Save } from "lucide-react";
+
+export function ConfigTab({
+ dnpName,
+ setupWizard,
+ userSettings
+}: {
+ dnpName: string;
+ setupWizard?: SetupWizardType;
+ userSettings?: UserSettings;
+}) {
+ const [localUserSettings, setLocalUserSettings] = useState({});
+ const [submitting, setSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (userSettings) setLocalUserSettings({ [dnpName]: userSettings });
+ }, [userSettings, dnpName]);
+
+ const currentEnvs = localUserSettings[dnpName]?.environment || {};
+ const originalEnvs = userSettings?.environment || {};
+
+ // Flatten env values to a single-service map for display
+ const serviceNames = Object.keys(currentEnvs);
+
+ // Determine if there are changes
+ const hasChanges = useMemo(() => {
+ try {
+ return JSON.stringify(currentEnvs) !== JSON.stringify(originalEnvs);
+ } catch {
+ return false;
+ }
+ }, [currentEnvs, originalEnvs]);
+
+ function updateEnv(service: string, key: string, value: string) {
+ setLocalUserSettings((prev) => ({
+ ...prev,
+ [dnpName]: {
+ ...prev[dnpName],
+ environment: {
+ ...prev[dnpName]?.environment,
+ [service]: {
+ ...(prev[dnpName]?.environment?.[service] || {}),
+ [key]: value
+ }
+ }
+ }
+ }));
+ }
+
+ async function handleSubmit() {
+ const newEnvs = localUserSettings[dnpName]?.environment;
+ if (!newEnvs) return;
+
+ const diffEnvs = difference(originalEnvs, newEnvs);
+
+ // Build nice names from setup wizard fields
+ const firstServiceEnvs: PackageEnvs = Object.values(diffEnvs)[0] || {};
+ const niceNames = Object.keys(firstServiceEnvs).map((name) => {
+ for (const field of setupWizard?.fields || []) {
+ if (field.target?.type === "environment" && field.target.name === name) return field.title || name;
+ }
+ return name;
+ });
+
+ const envList = niceNames.join(", ");
+ const prettyName = prettyDnpName(dnpName);
+
+ try {
+ setSubmitting(true);
+ toast.loading(`Updating ${prettyName} ${envList}…`, { id: "config-update" });
+ await api.packageSetEnvironment({ dnpName, environmentByService: diffEnvs });
+ toast.success(`Updated ${prettyName} ${envList}`, { id: "config-update" });
+ } catch (e) {
+ toast.error(`Failed to update: ${e}`, { id: "config-update" });
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ // Build fields from setupWizard or fall back to raw env display
+ const fields = setupWizard?.fields || [];
+
+ // If we have setup wizard fields, render them nicely
+ if (fields.length > 0) {
+ return (
+
+
+
+
+
+ Configuration
+
+ Modify the environment variables for {prettyDnpName(dnpName)}.
+
+
+ {fields.map((field) => {
+ const target = field.target;
+ if (!target || target.type !== "environment") return null;
+
+ const rawService = target.service;
+ const service = Array.isArray(rawService) ? rawService[0] : rawService || serviceNames[0] || dnpName;
+ const envKey = target.name;
+ const value = currentEnvs[service]?.[envKey] ?? "";
+
+ // Determine field type
+ if (field.enum) {
+ return (
+
+
{field.title || envKey}
+ {field.description &&
{field.description}
}
+
updateEnv(service, envKey, v)}>
+
+
+
+
+ {field.enum.map((opt) => (
+
+ {opt}
+
+ ))}
+
+
+
+ );
+ }
+
+ if (field.pattern === "^(true|false)$") {
+ return (
+
+
+
{field.title || envKey}
+ {field.description && (
+
{field.description}
+ )}
+
+
updateEnv(service, envKey, checked ? "true" : "false")}
+ />
+
+ );
+ }
+
+ return (
+
+
{field.title || envKey}
+ {field.description &&
{field.description}
}
+
) => updateEnv(service, envKey, e.target.value)}
+ />
+ {field.pattern && value && !new RegExp(field.pattern).test(value) && (
+
+ {field.patternErrorMessage || `Must match pattern: ${field.pattern}`}
+
+ )}
+
+ );
+ })}
+
+
+
+
+ Update configuration
+
+
+
+
+ );
+ }
+
+ // Fallback: raw environment editor
+ return (
+
+ {serviceNames.map((service) => (
+
+
+
+
+ {serviceNames.length > 1 ? `${service} – Environment` : "Environment Variables"}
+
+
+
+ {Object.entries(currentEnvs[service] || {}).map(([key, val]) => (
+
+ {key}
+ updateEnv(service, key, e.target.value)} />
+
+ ))}
+
+
+ ))}
+
+
+
+ Update configuration
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/FileManagerTab.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/FileManagerTab.tsx
new file mode 100644
index 0000000000..c29b330220
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/FileManagerTab.tsx
@@ -0,0 +1,193 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { useLocation } from "react-router-dom";
+import { api, apiRoutes } from "api";
+import { toast } from "sonner";
+import { PackageContainer } from "@dappnode/types";
+import { prettyFullName } from "utils/format";
+import fileToDataUri from "utils/fileToDataUri";
+import humanFileSize from "utils/humanFileSize";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import { ServiceSelector } from "../components/ServiceSelector";
+import { Upload, Download, TriangleAlert } from "lucide-react";
+
+const FILE_SIZE_WARNING = 1e6; // 1 MB
+
+export function FileManagerTab({ containers }: { containers: PackageContainer[] }) {
+ const serviceNames = containers.map((c) => c.serviceName);
+ const [serviceName, setServiceName] = useState(serviceNames[0]);
+ const location = useLocation();
+ const { from, to } = parseSearchParams(location.search);
+ const container = containers.find((c) => c.serviceName === serviceName);
+
+ return (
+
+ {/* Service selector */}
+ {serviceNames.length > 1 && (
+
+ )}
+
+ {container && (
+ <>
+
+
+ >
+ )}
+
+ );
+}
+
+/* ── Upload (Copy To) ───────────────────────────────────────────────── */
+
+function UploadCard({ container, toPathDefault }: { container: PackageContainer; toPathDefault?: string }) {
+ const [file, setFile] = useState();
+ const [toPath, setToPath] = useState("");
+
+ useEffect(() => {
+ if (toPathDefault) setToPath(toPathDefault);
+ }, [toPathDefault]);
+
+ async function handleUpload() {
+ if (!file) return;
+ const prettyName = prettyFullName(container);
+ try {
+ toast.loading(`Uploading ${file.name} to ${prettyName}…`, { id: "file-upload" });
+ const dataUri = await fileToDataUri(file);
+ await api.copyFileToDockerContainer({
+ containerName: container.containerName,
+ dataUri,
+ filename: file.name || "",
+ toPath
+ });
+ toast.success(`Uploaded ${file.name} to ${prettyName} ${toPath}`, { id: "file-upload" });
+ } catch (e) {
+ toast.error(`Upload failed: ${e}`, { id: "file-upload" });
+ }
+ }
+
+ return (
+
+
+
+
+ Upload File
+
+ Upload a file from your computer to the container.
+
+
+ {/* File picker */}
+
+
Source file
+
{
+ if (e.target.files?.[0]) setFile(e.target.files[0]);
+ }}
+ />
+ {file && (
+
+ {file.name} ({humanFileSize(file.size)})
+
+ )}
+
+
+ {file && file.size > FILE_SIZE_WARNING && (
+
+
+
+ This tool is not meant for large file transfers. Expect unstable behaviour for files over 1 MB.
+
+
+ )}
+
+ {/* Destination path */}
+
+
Destination path
+
+ setToPath(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && handleUpload()}
+ className="tw:flex-1"
+ />
+
+
+ Upload
+
+
+
+
+
+ );
+}
+
+/* ── Download (Copy From) ───────────────────────────────────────────── */
+
+function DownloadCard({ container, fromPathDefault }: { container: PackageContainer; fromPathDefault?: string }) {
+ const [fromPath, setFromPath] = useState("");
+
+ const getUrl = useCallback(
+ (path: string) => apiRoutes.fileDownloadUrl({ containerName: container.containerName, path }),
+ [container.containerName]
+ );
+
+ const downloadFile = useCallback((path: string) => window.open(getUrl(path), "_newtab"), [getUrl]);
+
+ useEffect(() => {
+ if (fromPathDefault) {
+ setFromPath(fromPathDefault);
+ downloadFile(fromPathDefault);
+ }
+ }, [fromPathDefault, downloadFile]);
+
+ return (
+
+
+
+
+ Download File
+
+ Download a file from the container to your computer.
+
+
+
+
Container path
+
+
setFromPath(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && downloadFile(fromPath)}
+ className="tw:flex-1"
+ />
+
+
+
+ Download
+
+
+
+
+
+
+ );
+}
+
+/* ── Helpers ────────────────────────────────────────────────────────── */
+
+function parseSearchParams(searchQuery: string): { from?: string; to?: string } {
+ try {
+ if (!searchQuery) return {};
+ const sp = new URLSearchParams(searchQuery);
+ return {
+ from: sp.get("from") || undefined,
+ to: sp.get("to") || undefined
+ };
+ } catch {
+ return {};
+ }
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/LogsTab.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/LogsTab.tsx
new file mode 100644
index 0000000000..333fb57917
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/LogsTab.tsx
@@ -0,0 +1,153 @@
+import React, { useState, useEffect } from "react";
+import { api, apiRoutes } from "api";
+import { PackageContainer } from "@dappnode/types";
+import { stringIncludes, stringSplit } from "utils/strings";
+import { Card, CardContent, CardHeader, CardTitle } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Switch } from "components/primitives/switch";
+import { ServiceSelector } from "../components/ServiceSelector";
+import { Download, Search, Terminal as TerminalIcon } from "lucide-react";
+
+const REFRESH_INTERVAL = 2_000;
+const TERMINAL_ID = "ai-logs-terminal";
+const validateLines = (n: number) => !isNaN(n) && n > 0;
+
+export function LogsTab({ containers }: { containers: PackageContainer[] }) {
+ const serviceNames = containers.map((c) => c.serviceName);
+ const [serviceName, setServiceName] = useState(serviceNames[0]);
+ const [autoRefresh, setAutoRefresh] = useState(true);
+ const [timestamps, setTimestamps] = useState(false);
+ const [query, setQuery] = useState("");
+ const [lines, setLines] = useState(200);
+ const [logs, setLogs] = useState("");
+
+ const container = containers.find((c) => c.serviceName === serviceName);
+ const containerName = container?.containerName;
+
+ useEffect(() => {
+ let scrollToBottom = () => {
+ const el = document.getElementById(TERMINAL_ID);
+ if (el) el.scrollTop = el.scrollHeight;
+ scrollToBottom = () => {};
+ };
+ let unmounted = false;
+
+ async function fetchLogs() {
+ try {
+ if (!containerName) throw Error("No containerName");
+ const result = await api.packageLog({ containerName, options: { timestamps, tail: lines } });
+ if (typeof result !== "string") throw Error("Logs must be a string");
+ if (unmounted) return;
+ setLogs(result);
+ setTimeout(scrollToBottom, 10);
+ } catch (e) {
+ setLogs(`Error fetching logs: ${(e as Error).message}`);
+ setAutoRefresh(false);
+ }
+ }
+
+ setLogs("fetching...");
+ if (autoRefresh) {
+ const interval = setInterval(fetchLogs, REFRESH_INTERVAL);
+ fetchLogs();
+ return () => {
+ clearInterval(interval);
+ unmounted = true;
+ };
+ } else {
+ fetchLogs();
+ return () => {
+ unmounted = true;
+ };
+ }
+ }, [autoRefresh, timestamps, lines, containerName]);
+
+ // Filter
+ const logsArray = stringSplit(logs, /\r?\n/);
+ let logsFiltered = query ? logsArray.filter((line) => stringIncludes(line, query)).join("\n") : logs;
+ if (logs && query && !logsFiltered) logsFiltered = "No match found";
+
+ const terminalText = validateLines(lines) ? logsFiltered : "Lines must be a number > 0";
+
+ return (
+
+
+
+
+
+ Container Logs
+
+
+
+
+ {/* Service selector */}
+
+
+ {/* Controls row */}
+
+ {/* Auto-refresh */}
+
+
+
+ Auto-refresh
+
+
+
+ {/* Timestamps */}
+
+
+
+ Timestamps
+
+
+
+ {/* Lines */}
+
+
+ Lines
+
+ setLines(parseInt(e.target.value) || 0)}
+ className="tw:w-24"
+ />
+
+
+ {/* Download */}
+ {containerName && (
+
+
+
+ Download all
+
+
+ )}
+
+
+ {/* Search */}
+
+
+ setQuery(e.target.value)}
+ className="tw:pl-8"
+ />
+
+
+ {/* Terminal */}
+
+ {terminalText || "No logs available"}
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/info/ContainersCard.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/ContainersCard.tsx
new file mode 100644
index 0000000000..42390d69b7
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/ContainersCard.tsx
@@ -0,0 +1,194 @@
+import React, { useState } from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { InstalledPackageData, PackageContainer } from "@dappnode/types";
+import { Card, CardContent, CardHeader, CardTitle } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { parseContainerState, SimpleState } from "pages/packages/components/StateBadge/utils";
+import { prettyDnpName, prettyFullName } from "utils/format";
+import { continueIfCalleDisconnected } from "api/utils";
+import { wifiDnpName } from "@/params";
+import { RefreshCw, Pause, Play, ChevronDown, ChevronUp } from "lucide-react";
+
+export const stateColors: Record = {
+ running: "tw:text-green-600 tw:dark:text-green-400",
+ stopped: "tw:text-muted-foreground",
+ crashed: "tw:text-destructive",
+ restarting: "tw:text-amber-500",
+ removing: "tw:text-muted-foreground"
+};
+
+export function ContainersCard({ dnp }: { dnp: InstalledPackageData }) {
+ const [expanded, setExpanded] = useState(false);
+ const multiService = dnp.containers.length > 1;
+ const allRunning = dnp.containers.every((c) => c.running);
+ const isWifi = dnp.dnpName === wifiDnpName;
+
+ async function doRestart(container?: PackageContainer) {
+ const serviceNames = container ? [container.serviceName] : undefined;
+ const name = container ? prettyFullName(container) : prettyDnpName(dnp.dnpName);
+ try {
+ toast.loading(`Restarting ${name}…`, { id: `restart-${name}` });
+ await continueIfCalleDisconnected(
+ () => api.packageRestart({ dnpName: dnp.dnpName, serviceNames }),
+ dnp.dnpName
+ )();
+ toast.success(`Restarted ${name}`, { id: `restart-${name}` });
+ } catch (e) {
+ toast.error(`Failed to restart ${name}: ${e}`, { id: `restart-${name}` });
+ }
+ }
+
+ async function doStartStop(container?: PackageContainer) {
+ const serviceNames = container ? [container.serviceName] : undefined;
+ const name = container ? prettyFullName(container) : prettyDnpName(dnp.dnpName);
+ try {
+ toast.loading(`Toggling ${name}…`, { id: `toggle-${name}` });
+ await api.packageStartStop({ dnpName: dnp.dnpName, serviceNames });
+ toast.success(`Toggled ${name}`, { id: `toggle-${name}` });
+ } catch (e) {
+ toast.error(`Failed: ${e}`, { id: `toggle-${name}` });
+ }
+ }
+
+ function RestartButton({ container, showName }: { container?: PackageContainer; showName: string }) {
+ const needsConfirm = container ? container.running : dnp.containers.some((c) => c.running);
+
+ if (!needsConfirm) {
+ return (
+ doRestart(container)} title="Restart">
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Restart {showName}
+
+ If this package holds state it may be lost. Are you sure you want to restart?
+
+
+
+ Cancel
+ doRestart(container)}>Restart
+
+
+
+ );
+ }
+
+ function StartStopButton({ container, showName }: { container?: PackageContainer; showName: string }) {
+ const running = container ? container.running : allRunning;
+
+ if (!isWifi) {
+ return (
+ doStartStop(container)}
+ title={running ? "Pause" : "Start"}
+ >
+ {running ? : }
+
+ );
+ }
+
+ return (
+
+
+
+ {running ? : }
+
+
+
+
+
+ {running ? "Stop" : "Start"} {showName}
+
+
+ If you are connected via WiFi you will lose access. Make sure you have another way to connect before
+ proceeding.
+
+
+
+ Cancel
+ doStartStop(container)}>Continue
+
+
+
+ );
+ }
+
+ function ContainerRow({ container, showName }: { container?: PackageContainer; showName: string }) {
+ const state = container
+ ? parseContainerState(container)
+ : { state: allRunning ? ("running" as SimpleState) : ("stopped" as SimpleState) };
+
+ return (
+
+
+ {state.state}
+
+ {showName}
+
+
+
+ );
+ }
+
+ return (
+
+
+ Containers
+
+
+
+
+ {allRunning ? "running" : "stopped"}
+
+
+ {multiService ? "All containers" : prettyDnpName(dnp.dnpName)}
+
+
+ {multiService && (
+ setExpanded((e) => !e)}>
+ {expanded ? : }
+
+ )}
+
+
+
+
+
+ {expanded &&
+ dnp.containers.map((c) => (
+
+ ))}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/info/GettingStartedSection.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/GettingStartedSection.tsx
new file mode 100644
index 0000000000..e534e2103a
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/GettingStartedSection.tsx
@@ -0,0 +1,62 @@
+import React, { useState, useEffect } from "react";
+import { api } from "api";
+import ReactMarkdown from "react-markdown";
+import { Card, CardContent, CardHeader, CardTitle } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { X } from "lucide-react";
+
+export function GettingStartedSection({
+ dnpName,
+ gettingStarted,
+ gettingStartedShow
+}: {
+ dnpName: string;
+ gettingStarted?: string;
+ gettingStartedShow?: boolean;
+}) {
+ const [show, setShow] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ setShow(Boolean(gettingStartedShow));
+ }, [gettingStartedShow]);
+
+ async function dismiss() {
+ if (loading) return;
+ try {
+ setLoading(true);
+ setShow(false);
+ if (gettingStartedShow) await api.packageGettingStartedToggle({ dnpName, show: false });
+ } catch (e) {
+ console.error(`Error on packageGettingStartedToggle: ${e}`);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ if (!gettingStarted) return null;
+
+ if (!show) {
+ return (
+ setShow(true)}>
+ Show getting started guide
+
+ );
+ }
+
+ return (
+
+
+ Getting started
+
+
+
+
+
+
+ {gettingStarted}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/info/InfoTab.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/InfoTab.tsx
new file mode 100644
index 0000000000..3589f3528f
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/InfoTab.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { InstalledPackageDetailData } from "@dappnode/types";
+import { GettingStartedSection } from "./GettingStartedSection";
+import { VersionCard } from "./VersionCard";
+import { PackageSentDataCard } from "./PackageSentDataCard";
+import { ContainersCard } from "./ContainersCard";
+import { VolumesCard } from "./VolumesCard";
+import { RemovePackageCard } from "./RemovePackageCard";
+
+export function InfoTab({ dnp }: { dnp: InstalledPackageDetailData }) {
+ const { manifest, gettingStarted, gettingStartedShow } = dnp;
+
+ return (
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/info/PackageSentDataCard.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/PackageSentDataCard.tsx
new file mode 100644
index 0000000000..aef805ff03
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/PackageSentDataCard.tsx
@@ -0,0 +1,117 @@
+import React, { useState } from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Separator } from "components/primitives/separator";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { isSecret } from "utils/isSecret";
+import { isLink } from "utils/isLink";
+import { Copy, Eye, EyeOff } from "lucide-react";
+
+export function PackageSentDataCard({ dnpName, data }: { dnpName: string; data: Record }) {
+ const entries = Object.entries(data).sort((a, b) =>
+ a[0].localeCompare(b[0], undefined, { numeric: true, sensitivity: "base" })
+ );
+
+ if (entries.length === 0) return null;
+
+ async function handleDelete() {
+ const toastId = toast.loading("Deleting sent data…");
+ try {
+ await api.packageSentDataDelete({ dnpName });
+ toast.success("Deleted sent data", { id: toastId });
+ } catch (e) {
+ toast.error(`Failed: ${e}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Package Sent Data
+ Values provided by the package at runtime.
+
+
+ {entries.map(([key, value]) => (
+
+ ))}
+
+
+
+
+ Delete all sent data
+
+
+
+
+ Delete sent data
+
+ Are you sure you want to delete all package-sent data? This action cannot be undone.
+
+
+
+ Cancel
+
+ Delete
+
+
+
+
+
+
+ );
+}
+
+function SentDataRow({ label, value }: { label: string; value: string }) {
+ const secret = isSecret(label);
+ const link = isLink(value);
+ const [visible, setVisible] = useState(!secret);
+ const [copied, setCopied] = useState(false);
+
+ function copyValue() {
+ navigator.clipboard.writeText(value);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ }
+
+ return (
+
+
{label}
+
+ {link ? (
+
+ {value}
+
+ ) : (
+
{visible ? value : "••••••••••"}
+ )}
+ {!link && secret && (
+
setVisible((v) => !v)}>
+ {visible ? : }
+
+ )}
+ {!link && (
+
+ {copied ? ✓ : }
+
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/info/RemovePackageCard.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/RemovePackageCard.tsx
new file mode 100644
index 0000000000..741067fb35
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/RemovePackageCard.tsx
@@ -0,0 +1,93 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { api } from "api";
+import { toast } from "sonner";
+import { InstalledPackageDetailData } from "@dappnode/types";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { prettyDnpName } from "utils/format";
+import { Trash2 } from "lucide-react";
+
+export function RemovePackageCard({ dnp }: { dnp: InstalledPackageDetailData }) {
+ const navigate = useNavigate();
+ const { dnpName, areThereVolumesToRemove, dependantsOf, notRemovable, manifest } = dnp;
+
+ if (notRemovable) return null;
+
+ const removeWarnings = manifest?.warnings?.onRemove;
+ const depList = dependantsOf.length > 0 ? dependantsOf.map((d) => prettyDnpName(d)).join(", ") : null;
+
+ async function doRemove(deleteVolumes: boolean) {
+ const prettyName = prettyDnpName(dnpName);
+ try {
+ toast.loading(`Removing ${prettyName}${deleteVolumes ? " and data" : ""}…`, { id: "pkg-rm" });
+ await api.packageRemove({ dnpName, deleteVolumes });
+ toast.success(`Removed ${prettyName}`, { id: "pkg-rm" });
+ navigate("/ai/packages");
+ } catch (e) {
+ toast.error(`Failed: ${e}`, { id: "pkg-rm" });
+ }
+ }
+
+ return (
+
+
+
+
Remove package
+
Delete {prettyDnpName(dnpName)} permanently.
+
+
+
+
+
+
+ Remove
+
+
+
+
+ Remove {prettyDnpName(dnpName)}
+
+ This action cannot be undone.
+ {removeWarnings && {removeWarnings} }
+ {depList && (
+
+ Warning: {depList} depend on {prettyDnpName(dnpName)} and may stop working.
+
+ )}
+ {areThereVolumesToRemove && (
+
+ If you do NOT want to keep the package's data, choose "Remove & delete data".
+
+ )}
+
+
+
+ Cancel
+ doRemove(false)}>
+ Remove
+
+ {areThereVolumesToRemove && (
+ doRemove(true)}>
+ Remove & delete data
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/info/VersionCard.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/VersionCard.tsx
new file mode 100644
index 0000000000..e59a9e0bbc
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/VersionCard.tsx
@@ -0,0 +1,93 @@
+import React from "react";
+import { InstalledPackageDetailData, Manifest, upstreamVersionToString } from "@dappnode/types";
+import { Card, CardContent, CardHeader, CardTitle } from "components/primitives/card";
+import { Separator } from "components/primitives/separator";
+import { ExternalLink, Home, Settings, Bug, Globe } from "lucide-react";
+
+const ipfsGatewayUrl = "http://ipfs.dappnode:8080";
+
+export function VersionCard({ dnp, manifest }: { dnp: InstalledPackageDetailData; manifest?: Manifest }) {
+ const { version, origin } = dnp;
+ const { upstreamVersion, upstream, links, bugs } = manifest || {};
+
+ const parsedUpstream = upstreamVersionToString({ upstreamVersion, upstream });
+ const linksArray = buildLinksArray(links, bugs);
+
+ return (
+
+
+ Package Info
+
+
+ {/* Version */}
+
+
Version
+
+ {version}
+ {parsedUpstream && ` (${parsedUpstream} upstream)`}
+
+ {origin && (
+
+ {origin}
+
+ )}
+
+
+ {/* Links */}
+ {linksArray.length > 0 && (
+ <>
+
+
+ {linksArray.map(({ name, url, icon: Icon }) => (
+
+
+ {name}
+
+ ))}
+
+ >
+ )}
+
+
+ );
+}
+
+function buildLinksArray(
+ links: Manifest["links"],
+ bugs: Manifest["bugs"]
+): { name: string; url: string; icon: React.FC<{ className?: string }> }[] {
+ const arr: { name: string; url: string; icon: React.FC<{ className?: string }> }[] = [];
+
+ const linksObj = typeof links === "string" ? { homepage: links } : typeof links === "object" ? links : {};
+
+ for (const [name, url] of Object.entries(linksObj || {})) {
+ if (!url) continue;
+ const icon =
+ name === "homepage" || name === "ui" || name === "webui"
+ ? Home
+ : name === "gateway"
+ ? Globe
+ : name === "api" || name === "apiEngine" || name === "engineAPI"
+ ? Settings
+ : ExternalLink;
+ arr.push({ name, url, icon });
+ }
+
+ if (bugs?.url) {
+ arr.push({ name: "Report bug", url: bugs.url, icon: Bug });
+ }
+
+ arr.sort((a) => (a.name === "homepage" ? -1 : 0));
+ return arr;
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/info/VolumesCard.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/VolumesCard.tsx
new file mode 100644
index 0000000000..3295780b43
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/VolumesCard.tsx
@@ -0,0 +1,125 @@
+import React, { useState } from "react";
+import { api } from "api";
+import { useSelector } from "react-redux";
+import { getVolumes } from "services/dappnodeStatus/selectors";
+import { toast } from "sonner";
+import { flatten, uniqBy, orderBy } from "lodash-es";
+import { InstalledPackageDetailData } from "@dappnode/types";
+import { Card, CardContent, CardHeader, CardTitle } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { prettyDnpName, prettyVolumeName, prettyBytes } from "utils/format";
+import { Trash2, ChevronDown, ChevronUp, HardDrive } from "lucide-react";
+
+export function VolumesCard({ dnp }: { dnp: InstalledPackageDetailData }) {
+ const [expanded, setExpanded] = useState(false);
+ const volumesData = useSelector(getVolumes);
+
+ const volumes = uniqBy(flatten(dnp.containers.map((c) => c.volumes)), (v) => v.name)
+ .filter((v) => v.name)
+ .sort((v1) => (v1.name ? -1 : 1))
+ .sort((v1) => ((v1.name || "").includes("data") ? -1 : 0))
+ .map(({ name, container, host }) => {
+ const vd = volumesData.find((v) => v.name === name);
+ const prettyVol = prettyVolumeName(name || "", dnp.dnpName);
+ const prettyStr = [prettyVol.owner, prettyVol.name].filter(Boolean).join(" – ");
+ return {
+ name: name || host,
+ prettyName: name ? prettyStr : container || "Unknown",
+ size: vd?.size
+ };
+ });
+
+ if (volumes.length === 0) return null;
+
+ const totalSize = volumes.reduce((s, v) => (v.size != null ? s + v.size : s), 0);
+
+ async function doRemoveVolumes(volumeName?: string) {
+ const prettyName = prettyDnpName(dnp.dnpName);
+ const toastId = `vol-rm-${volumeName || "all"}`;
+ try {
+ toast.loading(`Removing volumes of ${prettyName}…`, { id: toastId });
+ await api.packageRestartVolumes({ dnpName: dnp.dnpName, volumeId: volumeName });
+ toast.success(`Removed volumes of ${prettyName}`, { id: toastId });
+ } catch (e) {
+ toast.error(`Failed: ${e}`, { id: toastId });
+ }
+ }
+
+ function RemoveVolumeButton({ volumeName, label }: { volumeName?: string; label: string }) {
+ const prettyName = prettyDnpName(dnp.dnpName);
+ return (
+
+
+
+
+
+
+
+
+ Remove {label}
+
+ Remove {volumeName ? prettyDnpName(volumeName) : "all"} volume data for {prettyName}? This cannot be
+ undone. Blockchain nodes will re-sync from scratch.
+
+
+
+ Cancel
+ doRemoveVolumes(volumeName)}>
+ Remove
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ Volumes
+
+
+
+
+ All volumes
+ {prettyBytes(totalSize)}
+
+ {volumes.length > 1 && (
+ setExpanded((e) => !e)}>
+ {expanded ? : }
+
+ )}
+
+
+
+
+ {expanded &&
+ orderBy(volumes, ["size", "prettyName"], ["desc", "asc"]).map((vol) => (
+
+ {prettyDnpName(vol.prettyName)}
+
+ {typeof vol.size === "number" ? prettyBytes(vol.size) : "–"}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/info/index.ts b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/index.ts
new file mode 100644
index 0000000000..e3d3a8766b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/info/index.ts
@@ -0,0 +1 @@
+export { InfoTab } from "./InfoTab";
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/network/ContainerIpsCard.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/ContainerIpsCard.tsx
new file mode 100644
index 0000000000..e90ec58d9d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/ContainerIpsCard.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+import { PackageContainer } from "@dappnode/types";
+import { Card, CardContent, CardHeader, CardTitle } from "components/primitives/card";
+import { Badge } from "components/primitives/badge";
+import { Network as NetworkIcon } from "lucide-react";
+
+export function ContainerIpsCard({ container }: { container: PackageContainer }) {
+ return (
+
+
+
+
+ Container IPs
+
+
+
+ {!container.ip && !container.privateIp ? (
+ Container IPs not available
+ ) : (
+
+ {container.ip && (
+
+
+ Public
+
+ {container.ip}
+
+ )}
+ {container.privateIp && (
+
+
+ Private
+
+ {container.privateIp}
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/network/HttpsMappingsCard.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/HttpsMappingsCard.tsx
new file mode 100644
index 0000000000..9864112a94
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/HttpsMappingsCard.tsx
@@ -0,0 +1,264 @@
+import React, { useState } from "react";
+import { api, useApi } from "api";
+import { useSelector } from "react-redux";
+import { getDappnodeIdentityClean } from "services/dappnodeStatus/selectors";
+import { toast } from "sonner";
+import { HttpsPortalMapping } from "@dappnode/types";
+import { prettyFullName } from "utils/format";
+import { httpsPortalDnpName } from "params";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Switch } from "components/primitives/switch";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { Globe, Plus, Trash2, ArrowRight } from "lucide-react";
+
+export function HttpsMappingsCard({ dnpName, serviceName }: { dnpName: string; serviceName: string }) {
+ const [showAll, setShowAll] = useState(false);
+ const [editing, setEditing] = useState(false);
+ const [submitting, setSubmitting] = useState(false);
+ const [from, setFrom] = useState("");
+ const [port, setPort] = useState("80");
+ const [user, setUser] = useState("");
+ const [password, setPassword] = useState("");
+
+ const mappings = useApi.httpsPortalMappingsGet();
+ const dnpsRequest = useApi.packagesGet();
+ const dappnodeIdentity = useSelector(getDappnodeIdentityClean);
+
+ // Check if HTTPS Portal is installed
+ if (dnpsRequest.data) {
+ const httpsPortal = dnpsRequest.data.find((d) => d.dnpName === httpsPortalDnpName);
+ if (!httpsPortal) {
+ return (
+
+
+ HTTPS Domain Mapping
+
+
+
+ HTTPS Portal not installed
+ You must install the HTTPS Portal to use this feature.
+
+
+
+ );
+ }
+ }
+
+ if (!mappings.data) {
+ if (mappings.error) {
+ return (
+
+
+ HTTPS Domain Mapping
+
+
+
+ {mappings.error.message}
+
+
+
+ );
+ }
+ return (
+
+
+ HTTPS Domain Mapping
+
+
+ Loading mappings…
+
+
+ );
+ }
+
+ const serviceMappings = mappings.data.filter(
+ (m) => showAll || (m.dnpName === dnpName && m.serviceName === serviceName)
+ );
+
+ async function doAddMapping() {
+ if (submitting) return;
+
+ const mapping: HttpsPortalMapping = {
+ fromSubdomain: from,
+ dnpName,
+ serviceName,
+ port: parseInt(port),
+ auth: user && password ? { username: user, password } : undefined,
+ external: true
+ };
+
+ try {
+ setSubmitting(true);
+ toast.loading("Adding HTTPS mapping…", { id: "https-add" });
+ await api.httpsPortalMappingAdd({ mapping });
+ toast.success("Added HTTPS mapping", { id: "https-add" });
+ setFrom("");
+ setEditing(false);
+ mappings.revalidate();
+ } catch (e) {
+ toast.error(`Failed: ${e}`, { id: "https-add" });
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ async function doRemoveMapping(mapping: HttpsPortalMapping) {
+ try {
+ toast.loading("Removing mapping…", { id: "https-rm" });
+ await api.httpsPortalMappingRemove({ mapping });
+ toast.success("Removed mapping", { id: "https-rm" });
+ mappings.revalidate();
+ } catch (e) {
+ toast.error(`Failed: ${e}`, { id: "https-rm" });
+ }
+ }
+
+ return (
+
+
+
+
+ HTTPS Domain Mapping
+
+
+ Only expose pre-approved safe services. Custom mappings may introduce security risks.
+
+
+
+ {/* Mapping list */}
+ {serviceMappings.length === 0 ? (
+ No mappings
+ ) : (
+
+
+ Container
+
+ Subdomain
+ Auth
+
+
+ {serviceMappings.map((m) => (
+
+
+ {prettyFullName(m)} : {m.port}
+
+
+
+ {m.fromSubdomain}.{dappnodeIdentity.domain}
+
+
{m.auth ? m.auth.username : "–"}
+
+
+
+
+
+
+
+
+ Remove HTTPS mapping
+
+ Remove the mapping for {m.fromSubdomain}.{dappnodeIdentity.domain}?
+
+
+
+ Cancel
+ doRemoveMapping(m)}>
+ Remove
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Add mapping form */}
+ {editing && (
+
+
+
+
+
+
+ Add mapping
+
+
+
+
+ Expose service to the internet
+
+ Are you sure you want to expose this service to the public internet? This may introduce security
+ risks.
+
+
+
+ Cancel
+ Expose
+
+
+
+
setEditing(false)}>
+ Cancel
+
+
+
+ )}
+
+ {/* Bottom controls */}
+
+
+
+
+ Show all
+
+
+ {!editing && (
+
setEditing(true)}>
+ New mapping
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/network/NetworkTab.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/NetworkTab.tsx
new file mode 100644
index 0000000000..2dc6603e6d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/NetworkTab.tsx
@@ -0,0 +1,25 @@
+import React, { useState } from "react";
+import { PackageContainer } from "@dappnode/types";
+import { ServiceSelector } from "../../components/ServiceSelector";
+import { ContainerIpsCard } from "./ContainerIpsCard";
+import { PortMappingCard } from "./PortMappingCard";
+import { HttpsMappingsCard } from "./HttpsMappingsCard";
+
+export function NetworkTab({ containers }: { containers: PackageContainer[] }) {
+ const serviceNames = containers.map((c) => c.serviceName);
+ const [serviceName, setServiceName] = useState(serviceNames[0]);
+ const container = containers.find((c) => c.serviceName === serviceName);
+
+ return (
+
+ {serviceNames.length > 1 && (
+
+ )}
+ {container &&
}
+ {container && (
+
+ )}
+ {container &&
}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/network/PortMappingCard.tsx b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/PortMappingCard.tsx
new file mode 100644
index 0000000000..3e0225cc16
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/PortMappingCard.tsx
@@ -0,0 +1,184 @@
+import React, { useState, useEffect } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { PortMapping, PortProtocol } from "@dappnode/types";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "components/primitives/select";
+import { Globe, Plus, Trash2, TriangleAlert } from "lucide-react";
+import { portsToId, getHostPortMappings, findDuplicates, getPortErrors } from "./utils";
+
+export function PortMappingCard({
+ dnpName,
+ serviceName,
+ ports: portsFromDnp
+}: {
+ dnpName: string;
+ serviceName: string;
+ ports: PortMapping[];
+}) {
+ const [ports, setPorts] = useState([]);
+ const [updating, setUpdating] = useState(false);
+ const dnpInstalled = useApi.packagesGet();
+
+ useEffect(() => {
+ setPorts(
+ [...(portsFromDnp || [])]
+ .filter(({ host }) => host)
+ .sort((a, b) => {
+ if (a.deletable && !b.deletable) return 1;
+ if (!a.deletable && b.deletable) return -1;
+ return a.container - b.container;
+ })
+ );
+ }, [portsFromDnp]);
+
+ const hostPortMapping = getHostPortMappings(dnpInstalled.data || []);
+ const { errors, warnings } = getPortErrors(ports, dnpName, hostPortMapping);
+
+ async function handleUpdate() {
+ setUpdating(true);
+ try {
+ toast.loading(`Updating port mappings…`, { id: "port-update" });
+ await api.packageSetPortMappings({ dnpName, portMappingsByService: { [serviceName]: ports } });
+ toast.success(`Updated port mappings`, { id: "port-update" });
+ } catch (e) {
+ toast.error(`Failed: ${e}`, { id: "port-update" });
+ }
+ setUpdating(false);
+ }
+
+ function addPort() {
+ setPorts((ps) => [...ps, { container: 8000, protocol: PortProtocol.TCP, deletable: true }]);
+ }
+
+ function editPort(i: number, data: Partial) {
+ setPorts((ps) => ps.map((p, idx) => (idx === i ? { ...p, ...data } : p)));
+ }
+
+ function removePort(i: number) {
+ setPorts((ps) => ps.filter((p, idx) => !(idx === i && p.deletable)));
+ }
+
+ const duplicatedHost = findDuplicates(ports, "host");
+ const duplicatedContainer = findDuplicates(ports, "container");
+ const conflicting = ports.filter((p) => {
+ const owner = hostPortMapping[`${p.host}/${p.protocol}`];
+ return owner && owner !== dnpName;
+ });
+
+ const arePortsTheSame = portsToId(portsFromDnp) === portsToId(ports);
+ const hasInvalid = ports.some((p) => p.deletable && (!p.container || !p.protocol));
+ const disableUpdate =
+ hasInvalid ||
+ duplicatedHost.length > 0 ||
+ duplicatedContainer.length > 0 ||
+ conflicting.length > 0 ||
+ arePortsTheSame ||
+ updating;
+
+ return (
+
+
+
+
+ Public Port Mapping
+
+ Map container ports to host ports.
+
+
+
+
+ {errors.map((err) => (
+
+
+ {err}
+
+ ))}
+ {warnings.map((w) => (
+
+ ⚠ {w}
+
+ ))}
+
+
+
+ Update port mappings
+
+
+ New port
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/network/index.ts b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/index.ts
new file mode 100644
index 0000000000..e380e27dd1
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/index.ts
@@ -0,0 +1 @@
+export { NetworkTab } from "./NetworkTab";
diff --git a/packages/admin-ui/src/pages-new/ai/packages/tabs/network/utils.ts b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/utils.ts
new file mode 100644
index 0000000000..3e20bf09d5
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packages/tabs/network/utils.ts
@@ -0,0 +1,65 @@
+import { InstalledPackageData, PortMapping } from "@dappnode/types";
+import { prettyDnpName } from "utils/format";
+
+export const maxPortNumber = 65535;
+export const maxRegisteredPortNumber = 32768 - 1;
+export const wireguardPort = 51820;
+
+export function portsToId(portMappings: PortMapping[]): string {
+ return portMappings.map(({ host, container, protocol }) => [host, container, protocol].join("")).join("");
+}
+
+export function getHostPortMappings(dnps: InstalledPackageData[]) {
+ const map: Record = {};
+ for (const dnp of dnps)
+ for (const container of dnp.containers)
+ for (const port of container.ports || []) if (port.host) map[`${port.host}/${port.protocol}`] = dnp.dnpName;
+ return map;
+}
+
+export function findDuplicates(ports: PortMapping[], field: "host" | "container"): PortMapping[] {
+ const seen = new Set();
+ return ports.filter((p) => {
+ const val = p[field];
+ if (!val) return false;
+ const key = `${val}-${p.protocol}`;
+ if (seen.has(key)) return true;
+ seen.add(key);
+ return false;
+ });
+}
+
+export function getPortErrors(
+ ports: PortMapping[],
+ dnpName: string,
+ hostPortMapping: Record
+): { errors: string[]; warnings: string[] } {
+ const duplicatedHost = findDuplicates(ports, "host");
+ const duplicatedContainer = findDuplicates(ports, "container");
+ const conflicting = ports.filter((p) => {
+ const owner = hostPortMapping[`${p.host}/${p.protocol}`];
+ return owner && owner !== dnpName;
+ });
+
+ const errors: string[] = [];
+ duplicatedHost.forEach((p) => errors.push(`Duplicated host port ${p.host}/${p.protocol}`));
+ duplicatedContainer.forEach((p) => errors.push(`Duplicated container port ${p.container}/${p.protocol}`));
+ conflicting.forEach((p) => {
+ const owner = hostPortMapping[`${p.host}/${p.protocol}`];
+ errors.push(`Port ${p.host}/${p.protocol} is used by ${prettyDnpName(owner)}`);
+ });
+
+ const warnings: string[] = [];
+ ports.forEach((p) => {
+ if (
+ p.deletable &&
+ p.host &&
+ p.host > maxRegisteredPortNumber &&
+ p.host <= maxPortNumber &&
+ p.host !== wireguardPort
+ )
+ warnings.push(`Host port ${p.host}/${p.protocol} is in the ephemeral range`);
+ });
+
+ return { errors, warnings };
+}
diff --git a/packages/admin-ui/src/pages-new/ai/packagesConfig.ts b/packages/admin-ui/src/pages-new/ai/packagesConfig.ts
new file mode 100644
index 0000000000..227f5a8b4c
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/ai/packagesConfig.ts
@@ -0,0 +1,9 @@
+import { PackagesConfig } from "pages-new/packages/config";
+
+export const aiPackagesConfig: PackagesConfig = {
+ sectionLabel: "AI",
+ categoryFilter: { mode: "include", categories: ["AI"] },
+ packagesPath: "/ai/packages",
+ storePath: "/ai/store",
+ installerPath: "/installer"
+};
diff --git a/packages/admin-ui/src/pages-new/home/BannerNotifications.tsx b/packages/admin-ui/src/pages-new/home/BannerNotifications.tsx
new file mode 100644
index 0000000000..d9f76acded
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/BannerNotifications.tsx
@@ -0,0 +1,168 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { NavLink } from "react-router-dom";
+import ReactMarkdown from "react-markdown";
+import { api, useApi } from "api";
+import { Notification, Priority } from "@dappnode/types";
+import { dappmanagerAliases, externalUrlProps } from "params";
+import { resolveDappnodeUrl } from "utils/resolveDappnodeUrl";
+import { X, ChevronDown, ChevronUp } from "lucide-react";
+import { AlertDescription } from "components/primitives/alert";
+import { Button } from "components/primitives/button";
+import { Collapsible, CollapsibleContent } from "components/primitives/collapsible";
+import { cn } from "lib/utils";
+
+const NUM_BANNERS_SHOWN = 3;
+
+/** Priority-based background + text color classes */
+const priorityStyles: Record = {
+ [Priority.low]: "tw:bg-muted tw:text-muted-foreground",
+ [Priority.medium]: "tw:bg-primary/10 tw:text-primary",
+ [Priority.high]: "tw:bg-caution/15 tw:text-caution-foreground tw:dark:text-caution tw:dark:bg-caution/20",
+ [Priority.critical]: "tw:bg-destructive/10 tw:text-destructive tw:dark:bg-destructive/20"
+};
+
+/** Map Priority → CTA button variant */
+const priorityButtonVariant: Record = {
+ [Priority.low]: "default",
+ [Priority.medium]: "default",
+ [Priority.high]: "outline",
+ [Priority.critical]: "destructive"
+};
+
+/**
+ * Filters notifications:
+ * 1. Removes entries with errors
+ * 2. Deduplicates by correlationId, keeping the most recent
+ * 3. Keeps only triggered (not resolved) and unseen
+ * 4. Sorts by priority (critical → low)
+ */
+function filterNotifications(notifications: Notification[]): Notification[] {
+ const priorityOrder = [Priority.critical, Priority.high, Priority.medium, Priority.low];
+ const map = new Map();
+
+ notifications
+ .filter((n) => !n.errors)
+ .forEach((n) => {
+ const existing = map.get(n.correlationId);
+ if (!existing || new Date(n.timestamp) > new Date(existing.timestamp)) {
+ map.set(n.correlationId, n);
+ }
+ });
+
+ return Array.from(map.values())
+ .filter((n) => n.status === "triggered")
+ .filter((n) => !n.seen)
+ .sort((a, b) => priorityOrder.indexOf(a.priority) - priorityOrder.indexOf(b.priority));
+}
+
+/* ── Main banner list ───────────────────────────────────────────────── */
+
+export function BannerNotifications() {
+ const [notifications, setNotifications] = useState([]);
+
+ const oneMonthAgoTimestamp = useMemo(() => {
+ const now = new Date();
+ now.setMonth(now.getMonth() - 1);
+ return Math.floor(now.getTime() / 1000);
+ }, []);
+
+ const notificationsCall = useApi.notificationsGetBanner({ timestamp: oneMonthAgoTimestamp });
+
+ useEffect(() => {
+ if (notificationsCall.data) {
+ setNotifications(filterNotifications(notificationsCall.data));
+ }
+ }, [notificationsCall.data]);
+
+ // Revalidate every minute
+ useEffect(() => {
+ const interval = setInterval(() => notificationsCall.revalidate(), 60 * 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ if (!notifications.length) return null;
+
+ return (
+
+ {notifications.slice(0, NUM_BANNERS_SHOWN).map((n) => (
+ setNotifications((prev) => prev.filter((x) => x.id !== n.id))}
+ />
+ ))}
+
+ );
+}
+
+/* ── Single banner card ─────────────────────────────────────────────── */
+
+function BannerNotificationCard({ notification, onClose }: { notification: Notification; onClose: () => void }) {
+ const defaultOpen = notification.priority === Priority.critical;
+ const [open, setOpen] = useState(defaultOpen);
+
+ const handleClose = () => {
+ api.notificationSetSeenByCorrelationID({ correlationId: notification.correlationId });
+ onClose();
+ };
+
+ const isExternalUrl =
+ notification.callToAction && !dappmanagerAliases.some((alias) => notification.callToAction!.url.includes(alias));
+
+ const buttonVariant = priorityButtonVariant[notification.priority];
+
+ return (
+
+ setOpen((o) => !o)}
+ >
+ {/* Close button (top-right) */}
+
+ {
+ e.stopPropagation();
+ handleClose();
+ }}
+ aria-label="Dismiss notification"
+ >
+
+
+
+
+ {/* Title row */}
+
+ {notification.title}
+ {open ? : }
+
+
+ {/* Collapsible body */}
+
+
+ }}
+ />
+
+ {notification.callToAction && (
+
+
+ {notification.callToAction.title}
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/HomeLayout.tsx b/packages/admin-ui/src/pages-new/home/HomeLayout.tsx
new file mode 100644
index 0000000000..bc1a3393d2
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/HomeLayout.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+import { Routes, Route } from "react-router-dom";
+import { House, Settings, Globe, Bell } from "lucide-react";
+import { SectionLayout, NavItem } from "layouts";
+import { HomePage } from "./HomePage";
+import { SettingsPage } from "./settings/SettingsPage";
+import { EcosystemPage } from "./ecosystem";
+import { NotificationsPage } from "./notifications";
+import { InstallerPage } from "../ai/installer/InstallerPage";
+
+/* ── Route constants ────────────────────────────────────────────────── */
+
+export const homeBasePath = "/";
+export const homeInfoPath = "/info";
+export const homeSettingsPath = "/settings";
+export const homeEcosystemPath = "/ecosystem";
+export const homeNotificationsPath = "/notifications";
+
+/* ── Navigation items ───────────────────────────────────────────────── */
+
+const navItems: NavItem[] = [
+ { label: "Home", icon: House, path: homeBasePath },
+ { label: "Notifications", icon: Bell, path: homeNotificationsPath },
+ { label: "Settings", icon: Settings, path: homeSettingsPath },
+ { label: "Ecosystem", icon: Globe, path: homeEcosystemPath }
+];
+
+/* ── Layout ─────────────────────────────────────────────────────────── */
+
+export function HomeLayout() {
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/HomePage.tsx b/packages/admin-ui/src/pages-new/home/HomePage.tsx
new file mode 100644
index 0000000000..e5949dcd05
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/HomePage.tsx
@@ -0,0 +1,121 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { ClickableCard, CardHeader, CardTitle, CardDescription, CardContent } from "components/primitives/card";
+import { UserRoundKey, Sparkles, Check, ArrowRight } from "lucide-react";
+import dappnodeLogo from "img/dappnode-logo-only.png";
+import { LEGACY_BASE_PATH } from "utils/path";
+
+export function HomePage() {
+ const navigate = useNavigate();
+
+ return (
+
+ {/* Hero section */}
+
+
+ {/* ── Navigation cards ───────────────────────────────────── */}
+
+ {/* Staking Card */}
+
navigate("/staking")}>
+
+
+
+ Run nodes, validators and manage your staking setup among several networks.
+
+
+
+
+
+ Set-up and run nodes & configuration
+ Package management
+ Network & device settings
+
+
+
+
+
+
+
+ {/* AI Card */}
+
navigate("/ai")}>
+
+
+
+ Explore AI powered features to maximize your daily productivity with Dappnode.
+
+
+
+
+
+ Run models privately: local and cloud
+ Run and manage AI agents
+ Discover powerful AI tools
+
+
+
+
+
+
+
+
+ {/* ── Legacy UI link ──────────────────────────────────────── */}
+
+ Looking for the previous interface?{" "}
+ navigate(LEGACY_BASE_PATH)}
+ className="tw:underline tw:underline-offset-3 tw:text-primary tw:hover:text-primary/80 tw:cursor-pointer tw:bg-transparent"
+ >
+ Open Legacy UI
+
+
+
+ {/* ── Footer ───────────────────────────────────────────────── */}
+
+ Powered by Dappnode — decentralise everything
+
+
+ );
+}
+
+function FeatureItem({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+ {children}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/LoginPage.tsx b/packages/admin-ui/src/pages-new/home/LoginPage.tsx
new file mode 100644
index 0000000000..9cec9422f4
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/LoginPage.tsx
@@ -0,0 +1,167 @@
+import React, { useState } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+import { Lock, Eye, EyeOff } from "lucide-react";
+import { apiAuth } from "api";
+import { ReqStatus } from "types";
+import { NewPageLayout } from "pages-new/layouts";
+import { TypographyH1 } from "components/primitives/typography";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Separator } from "components/primitives/separator";
+import { Alert, AlertDescription, AlertTitle } from "components/primitives/alert";
+import { Spinner } from "components/primitives/spinner";
+import { ResetPasswordPage } from "./ResetPasswordPage";
+import dappnodeLogo from "img/dappnode-logo-wide-min.png";
+
+const loginRootPath = "/";
+const forgotPasswordPath = "/forgot-password";
+
+export function LoginPage({ refetchStatus }: { refetchStatus: () => Promise }) {
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [showPassword, setShowPassword] = useState(false);
+ const [reqStatus, setReqStatus] = useState({});
+
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ async function onLogin() {
+ try {
+ setReqStatus({ loading: true });
+ await apiAuth.login({ username, password });
+ setReqStatus({ result: true });
+
+ // Make sure user is properly logged in
+ const status = await apiAuth.fetchLoginStatus();
+ switch (status.status) {
+ case "logged-in":
+ break; // OK
+ case "not-logged-in":
+ if (status.noCookie) {
+ setReqStatus({ error: "Error logging in, cookies not enabled" });
+ } else {
+ setReqStatus({ error: "Error logging in, unknown error" });
+ }
+ break;
+ }
+ } catch (e) {
+ setReqStatus({ error: e as Error });
+ } finally {
+ refetchStatus();
+ }
+ }
+
+ function onForgotPassword() {
+ navigate(forgotPasswordPath);
+ }
+
+ async function onSuccessfulReset() {
+ await refetchStatus()?.catch(() => {});
+ navigate(loginRootPath);
+ }
+
+ // Use minimal router since there only one possible path
+ if (location.pathname === forgotPasswordPath) {
+ return ;
+ }
+
+ const errorMessage = reqStatus.error instanceof Error ? reqStatus.error.message : reqStatus.error;
+
+ return (
+
+
+
+
+ {/* Icon */}
+
+
+
+
+ {/* Title */}
+ Login
+
+ {/* Form */}
+
+
+ {/* Forgot password */}
+
+ Forgot password?
+
+
+ {/* Status messages */}
+ {reqStatus.result && (
+
+ Success
+ Logged in
+
+ )}
+ {reqStatus.error && (
+
+ Error
+ {errorMessage}
+
+ )}
+
+ {/* Footer */}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/NoConnectionPage.tsx b/packages/admin-ui/src/pages-new/home/NoConnectionPage.tsx
new file mode 100644
index 0000000000..ed8b1ddb60
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/NoConnectionPage.tsx
@@ -0,0 +1,73 @@
+import React from "react";
+import { WifiOff, MessageCircle, Github } from "lucide-react";
+import { NewPageLayout } from "pages-new/layouts";
+import { Card, CardContent } from "components/primitives/card";
+import { Separator } from "components/primitives/separator";
+import { Alert, AlertDescription, AlertTitle } from "components/primitives/alert";
+import { discordInviteUrl, githubNewIssueDappnodeUrl } from "params";
+import dappnodeLogo from "img/dappnode-logo-wide-min.png";
+
+export function NoConnectionPage({ error }: { error?: Error | string }) {
+ const errorMessage = error instanceof Error ? error.message : error;
+
+ return (
+
+
+
+
+ {/* Icon */}
+
+
+
+
+ {/* Title */}
+ No connection
+
+ {/* Description */}
+
+ Could not connect to Dappnode. Please make sure your VPN connection is still active. Otherwise, stop the
+ connection and reconnect and try accessing this page again.
+
+
+ {/* Error detail */}
+ {errorMessage && (
+
+ Connection error:
+ {errorMessage}
+
+ )}
+
+ {/* Help links */}
+
+ If the problems persist, please reach us via{" "}
+
+
+ Discord
+ {" "}
+ or{" "}
+
+
+ opening a Github issue
+
+ .
+
+
+ {/* Footer */}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/RegisterPage.tsx b/packages/admin-ui/src/pages-new/home/RegisterPage.tsx
new file mode 100644
index 0000000000..df3d711219
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/RegisterPage.tsx
@@ -0,0 +1,260 @@
+import React, { useState } from "react";
+import { Eye, EyeOff, ShieldCheck, Copy, Check, TriangleAlert } from "lucide-react";
+import { apiAuth } from "api";
+import { ReqStatus } from "types";
+import { validatePasswordsMatch, validateStrongPassword } from "utils/validation";
+import { NewPageLayout } from "pages-new/layouts";
+import { TypographyH1 } from "components/primitives/typography";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Separator } from "components/primitives/separator";
+import { Alert, AlertDescription, AlertTitle } from "components/primitives/alert";
+import { Spinner } from "components/primitives/spinner";
+import dappnodeLogo from "img/dappnode-logo-wide-min.png";
+
+export function RegisterPage({ refetchStatus }: { refetchStatus: () => Promise }) {
+ const [username, setUsername] = useState("admin");
+ const [password, setPassword] = useState("");
+ const [password2, setPassword2] = useState("");
+ const [showPassword, setShowPassword] = useState(false);
+ const [showPassword2, setShowPassword2] = useState(false);
+ const [recoveryToken, setRecoveryToken] = useState();
+ const [reqStatus, setReqStatus] = useState({});
+
+ const passwordError = validateStrongPassword(password);
+ const password2Error = validatePasswordsMatch(password, password2);
+ const isValid = password && password2 && !passwordError && !password2Error;
+
+ async function onRegister() {
+ if (isValid)
+ try {
+ setReqStatus({ loading: true });
+ const res = await apiAuth.register({ username, password });
+ setRecoveryToken(res.recoveryToken);
+ setReqStatus({ result: true });
+ } catch (e) {
+ setReqStatus({ error: e as Error });
+ }
+ }
+
+ function onCopiedRecoveryToken() {
+ refetchStatus()?.catch(() => {});
+ }
+
+ if (recoveryToken) {
+ return ;
+ }
+
+ const errorMessage = reqStatus.error instanceof Error ? reqStatus.error.message : reqStatus.error;
+
+ return (
+
+
+
+
+ {/* Icon */}
+
+
+
+
+ {/* Title */}
+ Login
+
+ {/* Description */}
+
+ Welcome! To protect your Dappnode register an admin password. It is recommended to use a password manager
+ to set a strong password.
+
+
+ {/* Form */}
+
+
+ {/* Status messages */}
+ {reqStatus.result && (
+
+ Success
+ Registered
+
+ )}
+ {reqStatus.error && (
+
+ Error
+ {errorMessage}
+
+ )}
+
+ {/* Footer */}
+
+
+
+
+
+
+ );
+}
+
+function CopyRecoveryToken({
+ recoveryToken,
+ onCopiedRecoveryToken
+}: {
+ recoveryToken: string;
+ onCopiedRecoveryToken: () => void;
+}) {
+ const [copied, setCopied] = useState(false);
+
+ async function handleCopy() {
+ try {
+ await navigator.clipboard.writeText(recoveryToken);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ // Fallback for insecure contexts
+ const textarea = document.createElement("textarea");
+ textarea.value = recoveryToken;
+ document.body.appendChild(textarea);
+ textarea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textarea);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ }
+
+ return (
+
+
+
+
+ {/* Icon */}
+
+
+
+
+ {/* Title */}
+ Recovery token
+
+ {/* Description */}
+
+ Store the recovery token in a safe place. If you lose your password it will allow you to reset the admin
+ account and register again.
+
+
+ {/* Recovery token box */}
+
+ {recoveryToken}
+
+ {copied ? : }
+
+
+
+ {/* Warning */}
+
+
+
+ If you also lose your recovery token you will have to directly access your machine.
+
+
+
+ {/* Confirm button */}
+
+ I've copied the recovery token
+
+
+ {/* Footer */}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/ResetPasswordPage.tsx b/packages/admin-ui/src/pages-new/home/ResetPasswordPage.tsx
new file mode 100644
index 0000000000..bd94288446
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/ResetPasswordPage.tsx
@@ -0,0 +1,135 @@
+import React, { useState } from "react";
+import { Lock, Eye, EyeOff, ExternalLink } from "lucide-react";
+import { apiAuth } from "api";
+import { ReqStatus } from "types";
+import { docsUrl } from "params";
+import { NewPageLayout } from "pages-new/layouts";
+import { TypographyH1 } from "components/primitives/typography";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Separator } from "components/primitives/separator";
+import { Alert, AlertDescription, AlertTitle } from "components/primitives/alert";
+import { Spinner } from "components/primitives/spinner";
+import dappnodeLogo from "img/dappnode-logo-wide-min.png";
+
+export function ResetPasswordPage({ onSuccessfulReset }: { onSuccessfulReset: () => void }) {
+ const [token, setToken] = useState("");
+ const [showToken, setShowToken] = useState(false);
+ const [reqStatus, setReqStatus] = useState({});
+
+ async function onReset() {
+ try {
+ setReqStatus({ loading: true });
+ await apiAuth.recoverPass({ token });
+ setReqStatus({ result: true });
+ onSuccessfulReset();
+ } catch (e) {
+ setReqStatus({ error: e as Error });
+ }
+ }
+
+ const errorMessage = reqStatus.error instanceof Error ? reqStatus.error.message : reqStatus.error;
+
+ return (
+
+
+
+
+ {/* Icon */}
+
+
+
+
+ {/* Title */}
+ Reset Password
+
+ {/* Description */}
+
+ Use your recovery token to reset the admin password and register again.
+
+
+ {/* Warning */}
+
+ Lost your recovery token?
+
+ If you have lost your password and recovery token you have to directly access your machine via SSH or by
+ connecting a keyboard and screen and follow{" "}
+
+ this guide
+
+
+ .
+
+
+
+ {/* Form */}
+
+
+ {/* Status messages */}
+ {reqStatus.result && (
+
+ Success
+ Password successfully reset.
+
+ )}
+ {reqStatus.error && (
+
+ Error
+ {errorMessage}
+
+ )}
+
+ {/* Footer */}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/ecosystem/CommunitySection.tsx b/packages/admin-ui/src/pages-new/home/ecosystem/CommunitySection.tsx
new file mode 100644
index 0000000000..f753e8d49e
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/ecosystem/CommunitySection.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+import { dappnodeDiscord, dappnodeGithub, givethDappnodeDonationsUrl } from "params";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { ExternalLink, Heart, Github } from "lucide-react";
+import { FaDiscord } from "react-icons/fa";
+
+interface CommunityLink {
+ title: string;
+ description: string;
+ icon: React.ComponentType<{ className?: string }>;
+ url: string;
+ urlLabel: string;
+}
+
+const communityLinks: CommunityLink[] = [
+ {
+ title: "Discord",
+ description:
+ "Get support, share your experience, and hang out with other Node Runners in the Dappnode Discord server.",
+ icon: FaDiscord,
+ url: dappnodeDiscord,
+ urlLabel: "Join Discord"
+ },
+ {
+ title: "Giveth Donations",
+ description:
+ "Support Dappnode's open-source mission with a donation on Giveth — every contribution makes a difference.",
+ icon: Heart,
+ url: givethDappnodeDonationsUrl,
+ urlLabel: "Go to Giveth"
+ },
+ {
+ title: "GitHub",
+ description: "Dappnode is Free Open Source Software. Review and contribute to its codebase on GitHub.",
+ icon: Github,
+ url: dappnodeGithub,
+ urlLabel: "View on GitHub"
+ }
+];
+
+export function CommunitySection() {
+ return (
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/ecosystem/DocsSection.tsx b/packages/admin-ui/src/pages-new/home/ecosystem/DocsSection.tsx
new file mode 100644
index 0000000000..dcc6ee0d95
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/ecosystem/DocsSection.tsx
@@ -0,0 +1,76 @@
+import React from "react";
+import { docsUrl } from "params";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { ExternalLink, BookOpen, Code } from "lucide-react";
+
+interface DocsCard {
+ title: string;
+ description: string;
+ highlights: string[];
+ icon: React.ComponentType<{ className?: string }>;
+ url: string;
+ urlLabel: string;
+}
+
+const docsCards: DocsCard[] = [
+ {
+ title: "User Documentation",
+ description:
+ "Everything you need to get started with your Dappnode — from initial setup and connecting to your node, to staking, managing packages, and troubleshooting common issues.",
+ highlights: ["Getting started & setup", "Access methods (VPN, Wi-Fi, Local)", "Staking & package management"],
+ icon: BookOpen,
+ url: docsUrl.userDocumentation,
+ urlLabel: "Browse User Docs"
+ },
+ {
+ title: "Developer Documentation",
+ description:
+ "Build and publish your own Dappnode packages. Learn about the SDK toolchain, the APM registry, package architecture, and how to contribute to the Dappnode ecosystem.",
+ highlights: ["SDK & CLI reference", "Package publishing workflow", "Architecture & contribution guides"],
+ icon: Code,
+ url: docsUrl.devsDocumentation,
+ urlLabel: "Browse Developer Docs"
+ }
+];
+
+export function DocsSection() {
+ return (
+
+ {docsCards.map((card) => (
+
+
+ {/* Icon + title */}
+
+
+ {/* Description */}
+ {card.description}
+
+ {/* Highlight bullets */}
+
+ {card.highlights.map((item) => (
+
+
+ {item}
+
+ ))}
+
+
+ {/* CTA */}
+
+
+ {card.urlLabel}
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/ecosystem/EcosystemPage.tsx b/packages/admin-ui/src/pages-new/home/ecosystem/EcosystemPage.tsx
new file mode 100644
index 0000000000..ea9a09cea4
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/ecosystem/EcosystemPage.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import { PageContainer, PageHeader } from "components/primitives/page";
+import { Separator } from "components/primitives/separator";
+import { TypographyH4 } from "components/primitives/typography";
+import { SdkSection } from "./SdkSection";
+import { CommunitySection } from "./CommunitySection";
+import { DocsSection } from "./DocsSection";
+
+export function EcosystemPage() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/ecosystem/SdkSection.tsx b/packages/admin-ui/src/pages-new/home/ecosystem/SdkSection.tsx
new file mode 100644
index 0000000000..2941fd4ac4
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/ecosystem/SdkSection.tsx
@@ -0,0 +1,123 @@
+import React from "react";
+import { sdkPublishAppUrl, docsUrl, sdkRepoUrl } from "params";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { ExternalLink, Globe, BookOpen, Shield, Database } from "lucide-react";
+
+interface SdkLink {
+ title: string;
+ description: string;
+ icon: React.ComponentType<{ className?: string }>;
+ url: string;
+ urlLabel: string;
+}
+
+const sdkGuides: SdkLink[] = [
+ {
+ title: "Package Publishing Guide",
+ description: "Learn how to create, build, and publish Dappnode packages to an APM registry.",
+ icon: BookOpen,
+ url: docsUrl.publishPkgGuide,
+ urlLabel: "Read Guide"
+ },
+ {
+ title: "Package Ownership Guide",
+ description: "Understand how package ownership works and how to manage repository permissions.",
+ icon: Shield,
+ url: docsUrl.ownershipPkgGuide,
+ urlLabel: "Read Guide"
+ }
+];
+
+export function SdkSection() {
+ return (
+
+ {/* APM registry explanation */}
+
+
+
+
+
+
+
Aragon Package Manager (APM) Registry
+
+ Dappnode packages are distributed through an on-chain registry powered by Aragon's APM smart
+ contracts on Ethereum mainnet. A public registry is deployed at{" "}
+
+ public.dappnode.eth
+
+ , where anyone can create their own repository and publish packages. The typical workflow involves
+ installing the SDK CLI via npm, initializing your Dappnode package, building its Docker image, and
+ publishing the release to the APM — all from the command line.
+
+
+
+
+
+
+ {/* SDK overview */}
+
+
+
+
+
+
+
+
Dappnode SDK-Publish
+
+ A web UI for publishing new package versions and managing repository permissions on the APM.
+
+
+
+
+
+ Open SDK Publish UI
+
+
+
+
+
+
+ {/* Guide links */}
+ {sdkGuides.map((guide) => (
+
+
+
+
+
+
+
+
{guide.title}
+
{guide.description}
+
+
+
+
+ {guide.urlLabel}
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/ecosystem/index.ts b/packages/admin-ui/src/pages-new/home/ecosystem/index.ts
new file mode 100644
index 0000000000..8c7ff4241d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/ecosystem/index.ts
@@ -0,0 +1 @@
+export { EcosystemPage } from "./EcosystemPage";
diff --git a/packages/admin-ui/src/pages-new/home/notifications/EndpointRow.tsx b/packages/admin-ui/src/pages-new/home/notifications/EndpointRow.tsx
new file mode 100644
index 0000000000..d77071d140
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/EndpointRow.tsx
@@ -0,0 +1,169 @@
+import React, { useCallback, useState } from "react";
+import { GatusEndpoint, CustomEndpoint } from "@dappnode/types";
+import { Switch } from "components/primitives/switch";
+import { Slider } from "components/primitives/slider";
+import { Separator } from "components/primitives/separator";
+import RenderMarkdown from "components/RenderMarkdown";
+
+/* ── Helpers ───────────────────────────────────────────────────────── */
+
+const OPERATORS = [">=", "<=", "<", ">", "==", "!="] as const;
+
+function parseConditionValue(condition: string): { operator: string | undefined; value: number } {
+ const operator = OPERATORS.find((op) => condition.includes(op));
+ if (!operator) return { operator: undefined, value: 0 };
+ const raw =
+ condition
+ .split(operator)
+ .pop()
+ ?.trim() || "0";
+ return { operator, value: parseFloat(raw) };
+}
+
+function buildUpdatedCondition(condition: string, operator: string | undefined, newValue: number): string {
+ if (!operator) return condition;
+ return `${condition.split(operator)[0].trim()} ${operator} ${newValue}`;
+}
+
+/* ── Shared endpoint row ───────────────────────────────────────────── */
+
+interface EndpointRowProps {
+ title: string;
+ description: string;
+ enabled: boolean;
+ metric?: { min: number; max: number; unit: string; sliderValue: number };
+ onToggle: () => void;
+ onSliderChange?: (value: number) => void;
+ onSliderCommit?: (value: number) => void;
+}
+
+function EndpointRow({
+ title,
+ description,
+ enabled,
+ metric,
+ onToggle,
+ onSliderChange,
+ onSliderCommit
+}: EndpointRowProps) {
+ return (
+
+
+
+ {enabled && metric && (
+
+ onSliderChange?.(v)}
+ onValueCommit={([v]) => onSliderCommit?.(v)}
+ className="tw:flex-1"
+ />
+
+ {metric.sliderValue} {metric.unit}
+
+
+ )}
+
+ );
+}
+
+/* ── Gatus endpoint item ───────────────────────────────────────────── */
+
+interface GatusEndpointRowProps {
+ endpoint: GatusEndpoint;
+ index: number;
+ setEndpoints: (updater: (prev: GatusEndpoint[]) => GatusEndpoint[]) => void;
+}
+
+function GatusEndpointRow({ endpoint, index, setEndpoints }: GatusEndpointRowProps) {
+ const { operator, value: conditionValue } = parseConditionValue(endpoint.conditions[0]);
+ const [sliderValue, setSliderValue] = useState(conditionValue);
+
+ const handleToggle = useCallback(() => {
+ setEndpoints((prev) => prev.map((ep, i) => (i === index ? { ...ep, enabled: !ep.enabled } : ep)));
+ }, [index, setEndpoints]);
+
+ const handleSliderCommit = useCallback(
+ (value: number) => {
+ setSliderValue(value);
+ const updatedCondition = buildUpdatedCondition(endpoint.conditions[0], operator, value);
+ setEndpoints((prev) =>
+ prev.map((ep, i) => (i === index ? { ...ep, conditions: [updatedCondition, ...ep.conditions.slice(1)] } : ep))
+ );
+ },
+ [index, endpoint.conditions, operator, setEndpoints]
+ );
+
+ return (
+
+ );
+}
+
+/* ── Custom endpoint item ──────────────────────────────────────────── */
+
+interface CustomEndpointRowProps {
+ endpoint: CustomEndpoint;
+ index: number;
+ setEndpoints: (updater: (prev: CustomEndpoint[]) => CustomEndpoint[]) => void;
+}
+
+function CustomEndpointRow({ endpoint, index, setEndpoints }: CustomEndpointRowProps) {
+ const [sliderValue, setSliderValue] = useState(endpoint.metric?.treshold || 0);
+
+ const handleToggle = useCallback(() => {
+ setEndpoints((prev) => prev.map((ep, i) => (i === index ? { ...ep, enabled: !ep.enabled } : ep)));
+ }, [index, setEndpoints]);
+
+ const handleSliderCommit = useCallback(
+ (value: number) => {
+ setSliderValue(value);
+ setEndpoints((prev) =>
+ prev.map((ep, i) => (i === index && ep.metric ? { ...ep, metric: { ...ep.metric, treshold: value } } : ep))
+ );
+ },
+ [index, setEndpoints]
+ );
+
+ return (
+
+ );
+}
+
+/* ── Exports ───────────────────────────────────────────────────────── */
+
+export { EndpointRow, GatusEndpointRow, CustomEndpointRow, Separator };
diff --git a/packages/admin-ui/src/pages-new/home/notifications/InboxTab.tsx b/packages/admin-ui/src/pages-new/home/notifications/InboxTab.tsx
new file mode 100644
index 0000000000..cdc3ea6e9c
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/InboxTab.tsx
@@ -0,0 +1,257 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { useApi, api } from "api";
+import { Search } from "lucide-react";
+import { Input } from "components/primitives/input";
+import { Badge } from "components/primitives/badge";
+import { Card, CardContent } from "components/primitives/card";
+import { Skeleton } from "components/primitives/skeleton";
+import { TypographyH4 } from "components/primitives/typography";
+import {
+ Pagination,
+ PaginationContent,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+ PaginationEllipsis
+} from "components/primitives/pagination";
+import { NotificationCard } from "./NotificationCard";
+
+const ITEMS_PER_PAGE = 15;
+
+export function InboxTab() {
+ const notifications = useApi.notificationsGetAll();
+
+ const [search, setSearch] = useState("");
+ const [categories, setCategories] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+
+ // Extract unique categories and mark all as seen
+ useEffect(() => {
+ if (!notifications.data) {
+ setCategories([]);
+ return;
+ }
+
+ const uniqueCategories = Array.from(new Set(notifications.data.map((n) => n.category).filter(Boolean)));
+ setCategories(uniqueCategories);
+ api.notificationsSetAllSeen();
+ }, [notifications.data]);
+
+ // Filter by search + category
+ const filteredNotifications = useMemo(() => {
+ if (!notifications.data) return [];
+
+ const healthy = notifications.data.filter((n) => !n.errors);
+
+ return healthy.filter(
+ (n) =>
+ (n.title.toLowerCase().includes(search.toLowerCase()) ||
+ n.dnpName.toLowerCase().includes(search.toLowerCase())) &&
+ (!selectedCategory || n.category === selectedCategory)
+ );
+ }, [search, notifications.data, selectedCategory]);
+
+ // Split into new / seen
+ const newNotifications = filteredNotifications
+ .filter((n) => !n.seen)
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
+
+ const seenNotifications = filteredNotifications
+ .filter((n) => n.seen)
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
+
+ const totalPages = Math.ceil(seenNotifications.length / ITEMS_PER_PAGE);
+
+ const paginatedSeen = useMemo(() => {
+ const start = (currentPage - 1) * ITEMS_PER_PAGE;
+ return seenNotifications.slice(start, start + ITEMS_PER_PAGE);
+ }, [seenNotifications, currentPage]);
+
+ // Reset page on filter change
+ useEffect(() => {
+ setCurrentPage(1);
+ }, [search, selectedCategory]);
+
+ /* ── Loading state ─────────────────────────────────────────────────── */
+
+ if (notifications.isValidating && !notifications.data) {
+ return (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ /* ── Render ────────────────────────────────────────────────────────── */
+
+ return (
+
+ {/* Search + category filters */}
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search by package name or notification title…"
+ className="tw:pl-9"
+ />
+
+
+ {categories.length > 0 && (
+
+ {categories.map((cat) => (
+ setSelectedCategory(cat === selectedCategory ? null : cat)}
+ >
+ {cat.charAt(0).toUpperCase() + cat.slice(1)}
+
+ ))}
+
+ )}
+
+
+ {/* New notifications */}
+ {newNotifications.length > 0 && (
+
+ New Notifications
+ {newNotifications.map((n) => (
+
+ ))}
+
+ )}
+
+ {/* History */}
+
+
History
+
+ {seenNotifications.length === 0 ? (
+
+ No notifications
+
+ ) : (
+ <>
+ {paginatedSeen.map((n) => (
+
+ ))}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+
+ {
+ e.preventDefault();
+ if (currentPage > 1) setCurrentPage((p) => p - 1);
+ }}
+ className={currentPage === 1 ? "tw:pointer-events-none tw:opacity-50" : ""}
+ />
+
+
+ {/* First page */}
+ {currentPage > 2 && (
+
+ {
+ e.preventDefault();
+ setCurrentPage(1);
+ }}
+ >
+ 1
+
+
+ )}
+
+ {currentPage > 3 && (
+
+
+
+ )}
+
+ {/* Previous page */}
+ {currentPage > 1 && (
+
+ {
+ e.preventDefault();
+ setCurrentPage((p) => p - 1);
+ }}
+ >
+ {currentPage - 1}
+
+
+ )}
+
+ {/* Current page */}
+
+ e.preventDefault()}>
+ {currentPage}
+
+
+
+ {/* Next page */}
+ {currentPage < totalPages && (
+
+ {
+ e.preventDefault();
+ setCurrentPage((p) => p + 1);
+ }}
+ >
+ {currentPage + 1}
+
+
+ )}
+
+ {currentPage < totalPages - 2 && (
+
+
+
+ )}
+
+ {/* Last page */}
+ {currentPage < totalPages - 1 && (
+
+ {
+ e.preventDefault();
+ setCurrentPage(totalPages);
+ }}
+ >
+ {totalPages}
+
+
+ )}
+
+
+ {
+ e.preventDefault();
+ if (currentPage < totalPages) setCurrentPage((p) => p + 1);
+ }}
+ className={currentPage === totalPages ? "tw:pointer-events-none tw:opacity-50" : ""}
+ />
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/notifications/NotificationCard.tsx b/packages/admin-ui/src/pages-new/home/notifications/NotificationCard.tsx
new file mode 100644
index 0000000000..c66d007992
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/NotificationCard.tsx
@@ -0,0 +1,146 @@
+import React, { useEffect, useState } from "react";
+import { NavLink } from "react-router-dom";
+import { Notification, Priority } from "@dappnode/types";
+import { ChevronDown } from "lucide-react";
+import { Card, CardContent } from "components/primitives/card";
+import { Badge } from "components/primitives/badge";
+import { Button } from "components/primitives/button";
+import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "components/primitives/collapsible";
+import { prettyDnpName } from "utils/format";
+import { api } from "api";
+import { dappmanagerAliases, externalUrlProps } from "params";
+import { resolveDappnodeUrl } from "utils/resolveDappnodeUrl";
+import RenderMarkdown from "components/RenderMarkdown";
+import dappnodeLogo from "img/dappnode-logo-only.png";
+
+/* ── Priority styling map ──────────────────────────────────────────── */
+
+const priorityConfig: Record<
+ Priority,
+ { label: string; variant: "default" | "secondary" | "destructive" | "caution" | "outline" }
+> = {
+ [Priority.low]: { label: "Informational", variant: "secondary" },
+ [Priority.medium]: { label: "Relevant", variant: "outline" },
+ [Priority.high]: { label: "Important", variant: "caution" },
+ [Priority.critical]: { label: "Critical", variant: "destructive" }
+};
+
+/* ── Helpers ───────────────────────────────────────────────────────── */
+
+function prettifiedBody(body: string): string {
+ if (body.includes("resolved: ")) return body.replace("resolved:", "Resolved:");
+ if (body.includes("triggered: ")) return body.replace("triggered:", "Attention:");
+ return body;
+}
+
+function formatTimestamp(ts: number): string {
+ return new Date(ts * 1000).toLocaleString(undefined, {
+ year: "numeric",
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit"
+ });
+}
+
+/* ── Component ─────────────────────────────────────────────────────── */
+
+interface NotificationCardProps {
+ notification: Notification;
+ openByDefault?: boolean;
+}
+
+export function NotificationCard({ notification, openByDefault = false }: NotificationCardProps) {
+ const [open, setOpen] = useState(openByDefault);
+
+ const avatar = notification.icon || dappnodeLogo;
+ const priority = priorityConfig[notification.priority];
+ const category = notification.category.charAt(0).toUpperCase() + notification.category.slice(1);
+
+ const isExternalUrl =
+ notification.callToAction && !dappmanagerAliases.some((alias) => notification.callToAction!.url.includes(alias));
+
+ // Auto-mark resolved banner notifications as seen
+ useEffect(() => {
+ if (!notification.seen && notification.isBanner && notification.status === "resolved") {
+ api.notificationSetSeenByCorrelationID({ correlationId: notification.correlationId });
+ }
+ }, []); // Run once on mount
+
+ return (
+
+
+
+
+ {/* Desktop: single row | Mobile: stacked */}
+
+ {/* Left group: avatar + text */}
+
+ {/* Avatar */}
+
+
+ {/* Text block */}
+
+ {/* Title + badges */}
+
+
{notification.title}
+
+ {priority.label}
+
+ {notification.status === "resolved" && (
+
+ Resolved
+
+ )}
+
+ {/* Meta: package · category · timestamp */}
+
+
+ {prettyDnpName(notification.dnpName)}
+
+ ·
+ {category}
+ ·
+ {formatTimestamp(notification.timestamp)}
+
+
+
+
+ {/* Right group: CTA + chevron */}
+
+ {notification.callToAction && (
+ e.stopPropagation()}
+ >
+
+ {notification.callToAction.title}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/notifications/NotificationsPage.tsx b/packages/admin-ui/src/pages-new/home/notifications/NotificationsPage.tsx
new file mode 100644
index 0000000000..5c012ab775
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/NotificationsPage.tsx
@@ -0,0 +1,125 @@
+import React from "react";
+import { NavLink, Routes, Route, Navigate } from "react-router-dom";
+import { useApi } from "api";
+import { notificationsDnpName } from "params";
+import { PageContainer, PageHeader } from "components/primitives/page";
+import {
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuLink
+} from "components/primitives/navigation-menu";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Skeleton } from "components/primitives/skeleton";
+import { AlertCircle } from "lucide-react";
+import { InboxTab } from "./InboxTab";
+import { SettingsTab } from "./SettingsTab";
+import { DevicesTab } from "./devices";
+import { withLegacyBase } from "utils/path";
+
+/* ── Tab definitions ────────────────────────────────────────────────── */
+
+interface NotificationsTabDef {
+ label: string;
+ subPath: string;
+ element: React.ReactNode;
+}
+
+function buildTabs(isInstalled: boolean): NotificationsTabDef[] {
+ const installPrompt = ;
+
+ return [
+ { label: "Inbox", subPath: "inbox", element: isInstalled ? : installPrompt },
+ { label: "Settings", subPath: "settings", element: isInstalled ? : installPrompt },
+ { label: "Devices", subPath: "devices", element: isInstalled ? : installPrompt }
+ ];
+}
+
+/* ── Placeholder components ─────────────────────────────────────────── */
+
+function InstallRequired() {
+ return (
+
+
+
+
+
Notifications package not installed
+
+ To receive notifications on your Dappnode, install the Notifications package.
+
+
+
+ Install Package
+
+
+
+ );
+}
+
+/* ── Main page ──────────────────────────────────────────────────────── */
+
+export function NotificationsPage() {
+ const pkgStatus = useApi.notificationsPackageStatus();
+
+ /* Loading state */
+ if (pkgStatus.isValidating && !pkgStatus.data) {
+ return (
+
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ /* Error state */
+ if (pkgStatus.error) {
+ const errorMsg = pkgStatus.error instanceof Error ? pkgStatus.error.message : String(pkgStatus.error);
+ return (
+
+
+
+
+ Error loading notifications status: {errorMsg}
+
+
+
+ );
+ }
+
+ const isInstalled = pkgStatus.data?.isInstalled ?? false;
+ const tabs = buildTabs(isInstalled);
+ const defaultTab = tabs[0]?.subPath ?? "inbox";
+
+ return (
+
+
+
+ {/* Tab navigation */}
+
+
+ {tabs.map((tab) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {/* Tab content */}
+
+ {tabs.map((tab) => (
+
+ ))}
+ } />
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/notifications/PackageEndpoints.tsx b/packages/admin-ui/src/pages-new/home/notifications/PackageEndpoints.tsx
new file mode 100644
index 0000000000..efc6a3748b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/PackageEndpoints.tsx
@@ -0,0 +1,115 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { GatusEndpoint, CustomEndpoint } from "@dappnode/types";
+import { Card, CardContent, CardHeader, CardTitle } from "components/primitives/card";
+import { Switch } from "components/primitives/switch";
+import { Badge } from "components/primitives/badge";
+import { Separator } from "components/primitives/separator";
+import { prettyDnpName } from "utils/format";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { GatusEndpointRow, CustomEndpointRow } from "./EndpointRow";
+
+interface PackageEndpointsProps {
+ dnpName: string;
+ gatusEndpoints: GatusEndpoint[];
+ customEndpoints: CustomEndpoint[];
+ isCore: boolean;
+}
+
+export function PackageEndpoints({ dnpName, gatusEndpoints, customEndpoints, isCore }: PackageEndpointsProps) {
+ const [endpointsGatus, setEndpointsGatus] = useState([...gatusEndpoints]);
+ const [endpointsCustom, setEndpointsCustom] = useState([...customEndpoints]);
+ const [pkgEnabled, setPkgEnabled] = useState(
+ gatusEndpoints.some((ep) => ep.enabled) || customEndpoints.some((ep) => ep.enabled)
+ );
+ const isUserAction = useRef(false);
+
+ // Check if package is running
+ const dnpCall = useApi.packageGet({ dnpName });
+ const allStopped = dnpCall.data?.containers.every((c) => c.state !== "running") ?? false;
+
+ // Sync with props
+ useEffect(() => {
+ setEndpointsGatus([...gatusEndpoints]);
+ setEndpointsCustom([...customEndpoints]);
+ setPkgEnabled(gatusEndpoints.some((ep) => ep.enabled) || customEndpoints.some((ep) => ep.enabled));
+ }, [gatusEndpoints, customEndpoints]);
+
+ // Toggle all endpoints for this package
+ const handlePkgToggle = useCallback(() => {
+ const next = !pkgEnabled;
+ isUserAction.current = true;
+ setEndpointsGatus((prev) => prev.map((ep) => ({ ...ep, enabled: next })));
+ setEndpointsCustom((prev) => prev.map((ep) => ({ ...ep, enabled: next })));
+ setPkgEnabled(next);
+ }, [pkgEnabled]);
+
+ // Wrap setters so any child toggle marks as user action
+ const setGatusWithFlag = useCallback((updater: (prev: GatusEndpoint[]) => GatusEndpoint[]) => {
+ isUserAction.current = true;
+ setEndpointsGatus(updater);
+ }, []);
+
+ const setCustomWithFlag = useCallback((updater: (prev: CustomEndpoint[]) => CustomEndpoint[]) => {
+ isUserAction.current = true;
+ PackageEndpoints;
+ setEndpointsCustom(updater);
+ }, []);
+
+ // Persist changes when user modifies endpoints
+ useEffect(() => {
+ if (!isUserAction.current) return;
+ isUserAction.current = false;
+
+ const prettyName = prettyDnpName(dnpName);
+
+ api
+ .notificationsUpdateEndpoints({
+ dnpName,
+ isCore,
+ notificationsConfig: {
+ endpoints: endpointsGatus.length > 0 ? endpointsGatus : undefined,
+ customEndpoints: endpointsCustom.length > 0 ? endpointsCustom : undefined
+ }
+ })
+ .then(() => toast.success(`${prettyName} settings updated`))
+ .catch(() => toast.error(`Error updating settings for ${prettyName}`));
+ }, [endpointsGatus, endpointsCustom, dnpName, isCore]);
+
+ const totalEndpoints = endpointsGatus.length + endpointsCustom.length;
+
+ return (
+
+
+
+ {prettyDnpName(dnpName)}
+ {allStopped && (
+
+ Not running
+
+ )}
+
+
+
+
+
+ {pkgEnabled && totalEndpoints > 0 && (
+
+ {endpointsGatus.map((ep, i) => (
+
+ {i > 0 && }
+
+
+ ))}
+
+ {endpointsCustom.map((ep, i) => (
+
+ {(i > 0 || endpointsGatus.length > 0) && }
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/notifications/SettingsTab.tsx b/packages/admin-ui/src/pages-new/home/notifications/SettingsTab.tsx
new file mode 100644
index 0000000000..a535f07997
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/SettingsTab.tsx
@@ -0,0 +1,123 @@
+import React, { useEffect, useState } from "react";
+import { useApi } from "api";
+import { GatusEndpoint, CustomEndpoint } from "@dappnode/types";
+import { Card, CardHeader, CardTitle, CardDescription } from "components/primitives/card";
+import { Switch } from "components/primitives/switch";
+import { Skeleton } from "components/primitives/skeleton";
+import { Separator } from "components/primitives/separator";
+import { TypographyH4 } from "components/primitives/typography";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle
+} from "components/primitives/alert-dialog";
+import { useHandleNotificationsPkg } from "hooks/useHandleNotificationsPkg";
+import { PackageEndpoints } from "./PackageEndpoints";
+
+/* ── Types ──────────────────────────────────────────────────────────── */
+
+interface EndpointsData {
+ [dnpName: string]: {
+ endpoints: GatusEndpoint[];
+ customEndpoints: CustomEndpoint[];
+ isCore: boolean;
+ };
+}
+
+/* ── Component ─────────────────────────────────────────────────────── */
+
+export function SettingsTab() {
+ const [endpointsData, setEndpointsData] = useState();
+ const [pauseDialogOpen, setPauseDialogOpen] = useState(false);
+ const endpointsCall = useApi.notificationsGetAllEndpoints();
+ const { isLoading: pkgLoading, isRunning, startStopNotificationsNoConfirm } = useHandleNotificationsPkg();
+
+ useEffect(() => {
+ if (endpointsCall.data) {
+ setEndpointsData(endpointsCall.data as EndpointsData);
+ }
+ }, [endpointsCall.data]);
+
+ /* ── Loading ─────────────────────────────────────────────────────── */
+
+ if (pkgLoading || endpointsCall.isValidating) {
+ return (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ /* ── Render ──────────────────────────────────────────────────────── */
+
+ return (
+
+ {/* Enable / Disable notifications */}
+
+
+
+ Enable notifications
+
+ Enable the notifications service to receive alerts about your Dappnode system.
+
+
+
+ {/* When running → show confirmation dialog before pausing */}
+ {isRunning ? (
+
+ setPauseDialogOpen(true)} />
+
+
+ Pause notifications package
+
+ The notifications package may alert you to critical issues. Pausing it could result in missing
+ important notifications.
+
+
+
+ Cancel
+ Pause
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {/* Package endpoint management */}
+ {isRunning && !pkgLoading && endpointsData && (
+ <>
+
+
+
+
Manage notifications
+
+ Enable, disable, and customize notifications for each installed package.
+
+
+
+
+ {Object.entries(endpointsData).map(([dnpName, data]) => (
+
+ ))}
+
+ >
+ )}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/notifications/devices/CurrentDeviceCard.tsx b/packages/admin-ui/src/pages-new/home/notifications/devices/CurrentDeviceCard.tsx
new file mode 100644
index 0000000000..b7af826fe4
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/devices/CurrentDeviceCard.tsx
@@ -0,0 +1,174 @@
+import React from "react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { Alert, AlertDescription, AlertTitle } from "components/primitives/alert";
+import { CheckCircle, Loader2, ShieldAlert, ShieldOff, Smartphone, Monitor, ExternalLink } from "lucide-react";
+import { docsUrl } from "params";
+
+/* ── Props ──────────────────────────────────────────────────────────── */
+
+interface CurrentDeviceCardProps {
+ isPwa: boolean;
+ isFullscreenOn: boolean;
+ permission: NotificationPermission | null;
+ permissionLoading: boolean;
+ isSubInNotifier: boolean;
+ isSubscribing: boolean;
+ pwaSubtabUrl: string | undefined;
+ device: string;
+ requestPermission: () => void;
+ subscribeBrowser: () => Promise;
+}
+
+/* ── Component ─────────────────────────────────────────────────────── */
+
+export function CurrentDeviceCard({
+ isPwa,
+ isFullscreenOn,
+ permission,
+ permissionLoading,
+ isSubInNotifier,
+ isSubscribing,
+ pwaSubtabUrl,
+ device,
+ requestPermission,
+ subscribeBrowser
+}: CurrentDeviceCardProps) {
+ const DeviceIcon = device === "Mobile" ? Smartphone : Monitor;
+
+ return (
+
+
+
+
+ Current Device
+
+ Manage push notification subscription for this device.
+
+ {renderContent()}
+
+ );
+
+ function renderContent() {
+ /* ── Fullscreen mode blocks management ──────────────────────────── */
+ if (isFullscreenOn) {
+ return (
+
+
+ Exit full screen mode
+
+ To manage your current device, please exit full screen mode. Some features may not work as expected.
+
+
+ );
+ }
+
+ /* ── Not in PWA → prompt to install app ─────────────────────────── */
+ if (!isPwa || !permission) {
+ return (
+
+
+
+
Open the Dappnode App
+
+ To check your device status, please open the Dappnode App.
+ {!isPwa && " If you haven't installed the app yet, click the button below."}
+
+
+ {pwaSubtabUrl && (
+
+
+
+ Install App
+
+
+ )}
+
+ );
+ }
+
+ /* ── Waiting for permission approval ────────────────────────────── */
+ if (permissionLoading) {
+ return (
+
+
+
Waiting for permissions approval…
+
+ );
+ }
+
+ /* ── Permission denied ──────────────────────────────────────────── */
+ if (permission === "denied") {
+ return (
+
+
+
+
Notifications permission denied
+
+ Grant notification permission for this App in your browser settings to receive notifications.
+
+
+
+
+
+ Check Docs
+
+
+
+ );
+ }
+
+ /* ── Permission not yet requested ───────────────────────────────── */
+ if (permission === "default") {
+ return (
+
+
+
+
Grant notification permission
+
+ To receive notifications on this device, grant the notification permission. Click the button below and
+ then click "Allow" in the browser pop-up.
+
+
+
Grant permission
+
+ );
+ }
+
+ /* ── Already subscribed ─────────────────────────────────────────── */
+ if (isSubInNotifier) {
+ return (
+
+
+
+
Your device is subscribed to push notifications.
+
Subscribed
+
+
+ );
+ }
+
+ /* ── Subscribing in progress ────────────────────────────────────── */
+ if (isSubscribing) {
+ return (
+
+
+
Subscribing device…
+
+ );
+ }
+
+ /* ── Not subscribed → offer to subscribe ────────────────────────── */
+ return (
+
+
+
+
Device not subscribed
+
Your device is not subscribed to push notifications.
+
+
Subscribe Device
+
+ );
+ }
+}
diff --git a/packages/admin-ui/src/pages-new/home/notifications/devices/DevicesTab.tsx b/packages/admin-ui/src/pages-new/home/notifications/devices/DevicesTab.tsx
new file mode 100644
index 0000000000..3a22973fad
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/devices/DevicesTab.tsx
@@ -0,0 +1,99 @@
+import React from "react";
+import { Card, CardContent } from "components/primitives/card";
+import { Skeleton } from "components/primitives/skeleton";
+import { TooltipProvider } from "components/primitives/tooltip";
+import { TypographyH4 } from "components/primitives/typography";
+import { Smartphone } from "lucide-react";
+import { useHandleSubscription } from "hooks/PWA/useHandleSubscription";
+import { usePwaInstall } from "pages/system/components/App/PwaInstallContext";
+import { usePwaSubtabUrl } from "hooks/PWA/usePwaSubtabUrl";
+import useDeviceInfo from "hooks/PWA/useDeviceInfo";
+import { CurrentDeviceCard } from "./CurrentDeviceCard";
+import { SubscriptionCard } from "./SubscriptionCard";
+
+/* ── Component ─────────────────────────────────────────────────────── */
+
+export function DevicesTab() {
+ const {
+ subscription: browserSub,
+ subscriptionsList,
+ isSubscribing,
+ isSubInNotifier,
+ deleteSubscription,
+ requestPermission,
+ permission,
+ permissionLoading,
+ subscribeBrowser,
+ revalidateSubs
+ } = useHandleSubscription();
+
+ const { isPwa, isFullscreenOn } = usePwaInstall();
+ const pwaSubtabUrl = usePwaSubtabUrl();
+ const { device, loading: deviceLoading } = useDeviceInfo();
+
+ /* ── Loading ─────────────────────────────────────────────────────── */
+
+ if (deviceLoading) {
+ return (
+
+
+
+
+ );
+ }
+
+ /* ── Render ──────────────────────────────────────────────────────── */
+
+ return (
+
+
+ {/* Current device section */}
+
+
+ {/* Subscribed devices list */}
+
+
+
Subscribed Devices
+
+ All devices receiving push notifications from your Dappnode.
+
+
+
+ {subscriptionsList && subscriptionsList.length > 0 ? (
+
+ {subscriptionsList.map((sub, index) => (
+
+ ))}
+
+ ) : (
+
+
+
+
+ No subscribed devices yet. Subscribe your current device to start receiving notifications.
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/notifications/devices/SubscriptionCard.tsx b/packages/admin-ui/src/pages-new/home/notifications/devices/SubscriptionCard.tsx
new file mode 100644
index 0000000000..38964810a2
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/devices/SubscriptionCard.tsx
@@ -0,0 +1,192 @@
+import React, { useEffect, useRef, useState } from "react";
+import { NotifierSubscription } from "@dappnode/types";
+import { api } from "api";
+import { Card, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { Input } from "components/primitives/input";
+import { Tooltip, TooltipContent, TooltipTrigger } from "components/primitives/tooltip";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle
+} from "components/primitives/alert-dialog";
+import { Pencil, Trash2, Send, Check, X } from "lucide-react";
+import { toast } from "sonner";
+
+/* ── Props ──────────────────────────────────────────────────────────── */
+
+interface SubscriptionCardProps {
+ sub: NotifierSubscription;
+ isCurrentDevice: boolean;
+ deleteSubscription: (endpoint: string) => Promise;
+ revalidateSubs: () => Promise;
+}
+
+/* ── Component ─────────────────────────────────────────────────────── */
+
+export function SubscriptionCard({ sub, isCurrentDevice, deleteSubscription, revalidateSubs }: SubscriptionCardProps) {
+ const [editing, setEditing] = useState(false);
+ const [newAlias, setNewAlias] = useState(sub.alias);
+ const [deleteOpen, setDeleteOpen] = useState(false);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (editing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [editing]);
+
+ async function handleUpdateAlias() {
+ if (!newAlias || newAlias === sub.alias) {
+ setEditing(false);
+ return;
+ }
+ try {
+ await api.notificationsUpdateSubAlias({ endpoint: sub.endpoint || "", alias: newAlias });
+ revalidateSubs();
+ toast.success("Device renamed");
+ } catch (error) {
+ console.error("Error updating alias:", error);
+ toast.error("Failed to rename device");
+ } finally {
+ setEditing(false);
+ }
+ }
+
+ function cancelEdit() {
+ setEditing(false);
+ setNewAlias(sub.alias);
+ }
+
+ async function handleSendTest() {
+ try {
+ await api.notificationsSendSubTest({ endpoint: sub.endpoint });
+ toast.success("Test notification sent");
+ } catch (error) {
+ console.error("Error sending test notification:", error);
+ toast.error("Failed to send test notification");
+ }
+ }
+
+ async function handleDelete() {
+ if (!sub.endpoint) return;
+ try {
+ await deleteSubscription(sub.endpoint);
+ toast.success("Device removed");
+ } catch (error) {
+ console.error("Error deleting subscription:", error);
+ toast.error("Failed to remove device");
+ }
+ }
+
+ return (
+
+
+ {/* Left: alias + current-device badge */}
+
+ {editing ? (
+ setNewAlias(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") handleUpdateAlias();
+ if (e.key === "Escape") cancelEdit();
+ }}
+ className="tw:h-8 tw:max-w-60"
+ />
+ ) : (
+ {sub.alias}
+ )}
+ {isCurrentDevice && !editing && This device }
+
+
+ {/* Right: action buttons */}
+
+ {editing ? (
+ <>
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+ Save
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+ Send test notification
+
+
+
+ setEditing(true)}>
+
+
+
+ Rename device
+
+ {sub.endpoint && (
+
+
+
+ setDeleteOpen(true)}
+ >
+
+
+
+ Remove device
+
+
+
+ Remove device
+
+ Are you sure you want to remove {sub.alias} ? This device will no longer receive
+ push notifications.
+
+
+
+ Cancel
+ Remove
+
+
+
+ )}
+ >
+ )}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/notifications/devices/index.ts b/packages/admin-ui/src/pages-new/home/notifications/devices/index.ts
new file mode 100644
index 0000000000..e89e8e2200
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/devices/index.ts
@@ -0,0 +1 @@
+export { DevicesTab } from "./DevicesTab";
diff --git a/packages/admin-ui/src/pages-new/home/notifications/index.ts b/packages/admin-ui/src/pages-new/home/notifications/index.ts
new file mode 100644
index 0000000000..1e4350756e
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/notifications/index.ts
@@ -0,0 +1 @@
+export { NotificationsPage } from "./NotificationsPage";
diff --git a/packages/admin-ui/src/pages-new/home/settings/SettingsPage.tsx b/packages/admin-ui/src/pages-new/home/settings/SettingsPage.tsx
new file mode 100644
index 0000000000..6dda180495
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/SettingsPage.tsx
@@ -0,0 +1,74 @@
+import React from "react";
+import { NavLink, Routes, Route, Navigate } from "react-router-dom";
+import { PageContainer, PageHeader } from "components/primitives/page";
+import {
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuLink
+} from "components/primitives/navigation-menu";
+import { InfoTab, WifiTab, VpnTab, UpdatesTab, AppTab, ProfileTab, HostTab, NetworkTab, IpfsTab, AdvancedTab } from ".";
+
+/* ── Tab definitions ────────────────────────────────────────────────── */
+
+interface SettingsTabDef {
+ label: string;
+ /** Route path pattern (may include wildcard for nested routes) */
+ subPath: string;
+ /** Clean path for the NavLink (no wildcard). Defaults to subPath. */
+ navPath?: string;
+ element: React.ReactNode;
+}
+
+const tabs: SettingsTabDef[] = [
+ { label: "Info", subPath: "info", element: },
+ { label: "WiFi", subPath: "wifi", element: },
+ { label: "VPN", subPath: "vpn/*", navPath: "vpn", element: },
+ { label: "Updates", subPath: "updates", element: },
+ { label: "App", subPath: "app", element: },
+ { label: "Profile", subPath: "profile", element: },
+ { label: "Host", subPath: "host", element: },
+ { label: "Network", subPath: "network", element: },
+ { label: "IPFS", subPath: "ipfs", element: },
+ { label: "Advanced", subPath: "advanced", element: }
+];
+
+/**
+ * Settings page — comprehensive system management with tab navigation.
+ *
+ * Houses all legacy system/support functionality, rebuilt with shadcn primitives.
+ * Each tab is its own route under `/settings/*`.
+ */
+export function SettingsPage() {
+ const defaultTab = tabs[0]?.navPath ?? tabs[0]?.subPath ?? "info";
+
+ return (
+
+
+
+ {/* Tab navigation */}
+
+
+ {tabs.map((tab) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {/* Tab content */}
+
+ {tabs.map((tab) => (
+
+ ))}
+ } />
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/advanced/AdvancedTab.tsx b/packages/admin-ui/src/pages-new/home/settings/advanced/AdvancedTab.tsx
new file mode 100644
index 0000000000..03aec6a91b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/advanced/AdvancedTab.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { Separator } from "components/primitives/separator";
+import { VolumesSection } from "./VolumesSection";
+import { ContentProviderSection } from "./ContentProviderSection";
+import { TrustedKeysSection } from "./TrustedKeysSection";
+import { ClearCacheSection } from "./ClearCacheSection";
+import { ClearMainDbSection } from "./ClearMainDbSection";
+
+export function AdvancedTab() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/advanced/ClearCacheSection.tsx b/packages/admin-ui/src/pages-new/home/settings/advanced/ClearCacheSection.tsx
new file mode 100644
index 0000000000..c4116005de
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/advanced/ClearCacheSection.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+
+export function ClearCacheSection() {
+ async function cleanCache() {
+ const toastId = toast.loading("Deleting cache...");
+ try {
+ await api.cleanCache();
+ toast.success("Cache deleted", { id: toastId });
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Clear Cache
+
+ Remove the local cache of APM entries, manifests, avatars, and user action logs.
+
+
+
+
+
+ Clear Cache Database
+
+
+
+ Delete cache
+
+ This action cannot be undone. You should only delete the cache in response to a problem.
+
+
+
+ Cancel
+ Clear Cache
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/advanced/ClearMainDbSection.tsx b/packages/admin-ui/src/pages-new/home/settings/advanced/ClearMainDbSection.tsx
new file mode 100644
index 0000000000..512cbebb4b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/advanced/ClearMainDbSection.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+
+export function ClearMainDbSection() {
+ async function cleanDb() {
+ const toastId = toast.loading("Deleting main database...");
+ try {
+ await api.cleanDb();
+ toast.success("Main database deleted", { id: toastId });
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Clear Main Database
+
+ Remove the local database containing dyndns identity, IP registry, telegram configuration and more.
+
+
+
+
+
+ Clear Main Database
+
+
+
+ Delete main database
+
+ This action cannot be undone. You should only delete the database in response to a problem.
+
+
+
+ Cancel
+ Delete
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/advanced/ContentProviderSection.tsx b/packages/admin-ui/src/pages-new/home/settings/advanced/ContentProviderSection.tsx
new file mode 100644
index 0000000000..feeed2d6bf
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/advanced/ContentProviderSection.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Switch } from "components/primitives/switch";
+import { Label } from "components/primitives/label";
+
+export function ContentProviderSection() {
+ const mirrorProviderReq = useApi.mirrorProviderGet();
+ const enabled = mirrorProviderReq.data?.enabled ?? false;
+
+ async function handleToggle() {
+ const toastId = toast.loading(`${enabled ? "Disabling" : "Enabling"} content provider...`);
+ try {
+ await api.mirrorProviderSet({ enabled: !enabled });
+ toast.success(`Content provider ${enabled ? "disabled" : "enabled"}`, { id: toastId });
+ mirrorProviderReq.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Content Provider
+
+ When enabled, packages are downloaded from the Dappnode content provider first instead of IPFS.
+
+
+
+
+ Use Dappnode Content Provider
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/advanced/TrustedKeysSection.tsx b/packages/admin-ui/src/pages-new/home/settings/advanced/TrustedKeysSection.tsx
new file mode 100644
index 0000000000..fa7f1103eb
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/advanced/TrustedKeysSection.tsx
@@ -0,0 +1,233 @@
+import React, { useState } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { releaseSignatureProtocols } from "@dappnode/types";
+import type { TrustedReleaseKey, ReleaseSignatureProtocol } from "@dappnode/types";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Badge } from "components/primitives/badge";
+import { Skeleton } from "components/primitives/skeleton";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue
+} from "components/primitives/select";
+import { Plus, X } from "lucide-react";
+
+export function TrustedKeysSection() {
+ const [addingKey, setAddingKey] = useState(false);
+ const trustedKeys = useApi.releaseTrustedKeyList();
+
+ return (
+
+
+ Trusted Release Keys
+
+ Manage the cryptographic keys used to verify the authenticity of Dappnode package
+ releases.
+
+
+
+ {trustedKeys.isValidating && !trustedKeys.data && (
+
+ )}
+
+ {trustedKeys.data && trustedKeys.data.length > 0 && (
+
+
+
+
+ Name
+ Packages
+ Protocol
+
+
+
+
+ {trustedKeys.data.map((key) => (
+
+ {key.name}
+ {key.dnpNameSuffix}
+
+ {key.signatureProtocol}
+
+
+ trustedKeys.revalidate()}
+ />
+
+
+ ))}
+
+
+
+ )}
+
+ {trustedKeys.data?.length === 0 && (
+ No trusted keys configured.
+ )}
+
+ {addingKey ? (
+ {
+ setAddingKey(false);
+ trustedKeys.revalidate();
+ }}
+ onCancel={() => setAddingKey(false)}
+ />
+ ) : (
+ setAddingKey(true)}>
+
+ Add Key
+
+ )}
+
+
+ );
+}
+
+/* ── Helper components ─────────────────────────────────────────────── */
+
+function RemoveKeyButton({
+ keyName,
+ onRemoved
+}: {
+ keyName: string;
+ onRemoved: () => void;
+}) {
+ async function removeKey() {
+ const toastId = toast.loading("Removing trusted key...");
+ try {
+ await api.releaseTrustedKeyRemove(keyName);
+ toast.success("Key removed", { id: toastId });
+ onRemoved();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Remove key "{keyName}"?
+
+ Your Dappnode won't be able to safely verify releases signed by this key.
+
+
+
+ Cancel
+ Remove
+
+
+
+ );
+}
+
+function AddKeyForm({
+ onDone,
+ onCancel
+}: {
+ onDone: () => void;
+ onCancel: () => void;
+}) {
+ const [keyName, setKeyName] = useState("");
+ const [dnpNameSuffix, setDnpNameSuffix] = useState("");
+ const [signatureProtocol, setSignatureProtocol] = useState(releaseSignatureProtocols[0]);
+ const [key, setKey] = useState("");
+
+ async function addKey() {
+ const trustedKey: TrustedReleaseKey = {
+ name: keyName,
+ dnpNameSuffix,
+ signatureProtocol: signatureProtocol as ReleaseSignatureProtocol,
+ key
+ };
+ const toastId = toast.loading("Adding trusted key...");
+ try {
+ await api.releaseTrustedKeyAdd(trustedKey);
+ toast.success("Key added", { id: toastId });
+ onDone();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ const isValid = keyName && dnpNameSuffix && key;
+
+ return (
+
+
+ Key name
+ setKeyName(e.target.value)}
+ />
+
+
+ Package name suffix
+ setDnpNameSuffix(e.target.value)}
+ />
+
+
+ Signature protocol
+
+
+
+
+
+ {releaseSignatureProtocols.map((p) => (
+
+ {p}
+
+ ))}
+
+
+
+
+ Key
+ setKey(e.target.value)}
+ />
+
+
+
+ Submit Key
+
+
+ Cancel
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/advanced/VolumesSection.tsx b/packages/admin-ui/src/pages-new/home/settings/advanced/VolumesSection.tsx
new file mode 100644
index 0000000000..8ef064f4e9
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/advanced/VolumesSection.tsx
@@ -0,0 +1,136 @@
+import React, { useState } from "react";
+import { useSelector } from "react-redux";
+import { getVolumes } from "services/dappnodeStatus/selectors";
+import { volumeRemove, packageVolumeRemove } from "pages-new/utils/actions";
+import { getPrettyVolumeName, getPrettyVolumeOwner, prettyBytes } from "utils/format";
+import { parseStaticDate } from "utils/dates";
+import type { VolumeData } from "@dappnode/types";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { ChevronDown, ChevronUp, Trash2 } from "lucide-react";
+
+const MIN_VOLUME_SIZE = 10 * 1024 * 1024; // 10 MB
+
+export function VolumesSection() {
+ const [showAll, setShowAll] = useState(false);
+ const volumes = useSelector(getVolumes);
+
+ const getSize = (v: VolumeData) => v.size || v.fileSystem?.used || 0;
+ const sorted = [...volumes]
+ .sort((v1, v2) => getSize(v2) - getSize(v1))
+ .sort((v1, v2) => (v1.isOrphan && !v2.isOrphan ? -1 : 1));
+
+ const displayed = showAll ? sorted : sorted.filter((v) => getSize(v) > MIN_VOLUME_SIZE).slice(0, 5);
+
+ return (
+
+
+ Volumes
+
+ Docker volumes used by your Dappnode packages. Orphan volumes can be safely removed.
+
+
+
+ {displayed.length === 0 && (
+ No volumes found.
+ )}
+
+
+
+
+ Name
+ Size
+ Created
+ Remove
+
+
+
+ {displayed.map((vol) => {
+ const ownerPretty = getPrettyVolumeOwner(vol);
+ const namePretty = getPrettyVolumeName(vol);
+ const isDeletable = Boolean(vol.isOrphan || vol.owner);
+ const onDelete = vol.isOrphan
+ ? () => volumeRemove(vol.name)
+ : vol.owner
+ ? () => packageVolumeRemove(vol.owner!, vol.name)
+ : undefined;
+
+ return (
+
+
+
+ {ownerPretty && (
+ {ownerPretty} —
+ )}
+ {namePretty}
+ {vol.isOrphan && Orphan }
+
+
+
+ {prettyBytes(getSize(vol))}
+
+
+ {parseStaticDate(vol.createdAt, true)}
+
+
+ {isDeletable && onDelete && (
+
+
+
+
+
+
+
+
+ Remove volume
+
+ Are you sure you want to remove "{namePretty}"? This action
+ cannot be undone.
+
+
+
+ Cancel
+ Remove
+
+
+
+ )}
+
+
+ );
+ })}
+
+
+
+ setShowAll((x) => !x)}
+ className="tw:w-full"
+ >
+ {showAll ? (
+ <>
+ Show less
+ >
+ ) : (
+ <>
+ Show all ({volumes.length})
+ >
+ )}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/advanced/index.ts b/packages/admin-ui/src/pages-new/home/settings/advanced/index.ts
new file mode 100644
index 0000000000..b2441bc581
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/advanced/index.ts
@@ -0,0 +1 @@
+export { AdvancedTab } from "./AdvancedTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/app/AppTab.tsx b/packages/admin-ui/src/pages-new/home/settings/app/AppTab.tsx
new file mode 100644
index 0000000000..607eabc847
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/app/AppTab.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { withLegacyBase } from "utils/path";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Smartphone, ExternalLink } from "lucide-react";
+
+/**
+ * App tab — redirects to the legacy PWA-install page which requires
+ * special browser APIs and an HTTPS context that only the legacy
+ * router can provide.
+ */
+export function AppTab() {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ Dappnode App
+
+ The Dappnode app lets you connect to the Dappmanager on mobile or desktop and receive notifications.
+
+
+
+
+
+
+
Install as Progressive Web App
+
+ The PWA install flow requires a secure HTTPS context and specific browser APIs. Use the button below to
+ open the legacy install page where you can complete the setup.
+
+
+
+
+ navigate(withLegacyBase("system/app"))}>
+
+ Open PWA Install Page
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/app/index.ts b/packages/admin-ui/src/pages-new/home/settings/app/index.ts
new file mode 100644
index 0000000000..b653812773
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/app/index.ts
@@ -0,0 +1 @@
+export { AppTab } from "./AppTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/host/DiskExpansionSection.tsx b/packages/admin-ui/src/pages-new/home/settings/host/DiskExpansionSection.tsx
new file mode 100644
index 0000000000..95d870120a
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/host/DiskExpansionSection.tsx
@@ -0,0 +1,340 @@
+import React, { useState } from "react";
+import { api } from "api";
+import { HostHardDisk, HostVolumeGroup, HostLogicalVolume } from "@dappnode/types";
+import { dappnodeVolumeGroup, dappnodeLogicalVolume, forumUrl } from "params";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "components/primitives/select";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { HardDrive, ExternalLink, CheckCircle2, Loader2, TriangleAlert } from "lucide-react";
+
+type ReqStatus = { loading?: boolean; result?: T; error?: unknown };
+
+export function DiskExpansionSection() {
+ const [mode, setMode] = useState<"idle" | "automatic" | "manual">("idle");
+
+ // Request states
+ const [diskReq, setDiskReq] = useState>({});
+ const [volumeGroupReq, setVolumeGroupReq] = useState>({});
+ const [logicalVolumeReq, setLogicalVolumeReq] = useState>({});
+ const [expandReq, setExpandReq] = useState>({});
+
+ // Selected values
+ const [disk, setDisk] = useState("");
+ const [volumeGroup, setVolumeGroup] = useState("");
+ const [logicalVolume, setLogicalVolume] = useState("");
+
+ /* ── API calls ──────────────────────────────────────────────────── */
+
+ async function getDisks() {
+ try {
+ setDiskReq({ loading: true });
+ const disks = await api.lvmhardDisksGet();
+ if (disks[0]) setDisk(disks[0].name);
+ setDiskReq({ result: disks });
+ } catch (e) {
+ setDiskReq({ error: e });
+ }
+ }
+
+ async function getVolumeGroups() {
+ try {
+ setVolumeGroupReq({ loading: true });
+ const vgs = await api.lvmVolumeGroupsGet();
+ if (vgs[0]) setVolumeGroup(vgs[0].vg_name);
+ setVolumeGroupReq({ result: vgs });
+ } catch (e) {
+ setVolumeGroupReq({ error: e });
+ }
+ }
+
+ async function getLogicalVolumes() {
+ try {
+ setLogicalVolumeReq({ loading: true });
+ const lvs = await api.lvmLogicalVolumesGet();
+ if (lvs[0]) setLogicalVolume(lvs[0].lv_name);
+ setLogicalVolumeReq({ result: lvs });
+ } catch (e) {
+ setLogicalVolumeReq({ error: e });
+ }
+ }
+
+ async function getDappnodeDefaults() {
+ try {
+ const vgs = await api.lvmVolumeGroupsGet();
+ const defaultVg = vgs.find((vg) => vg.vg_name === dappnodeVolumeGroup)?.vg_name;
+ if (!defaultVg) throw Error(`Default volume group "${dappnodeVolumeGroup}" not found`);
+ setVolumeGroup(defaultVg);
+
+ const lvs = await api.lvmLogicalVolumesGet();
+ const defaultLv = lvs.find((lv) => lv.lv_name === dappnodeLogicalVolume)?.lv_name;
+ if (!defaultLv) throw Error(`Default logical volume "${dappnodeLogicalVolume}" not found`);
+ setLogicalVolume(defaultLv);
+ } catch (e) {
+ setExpandReq({ error: e });
+ }
+ }
+
+ async function expandDisk() {
+ try {
+ setExpandReq({ loading: true });
+ const result = await api.lvmDiskSpaceExtend({ disk, volumeGroup, logicalVolume });
+ setExpandReq({ result });
+ } catch (e) {
+ setExpandReq({ error: e });
+ }
+ }
+
+ function cleanAndSetMode(newMode: "automatic" | "manual") {
+ setDisk("");
+ setVolumeGroup("");
+ setLogicalVolume("");
+ setDiskReq({});
+ setVolumeGroupReq({});
+ setLogicalVolumeReq({});
+ setExpandReq({});
+ setMode(newMode);
+ }
+
+ /* ── Helpers ────────────────────────────────────────────────────── */
+
+ const expandError = expandReq.error
+ ? expandReq.error instanceof Error
+ ? expandReq.error.message
+ : String(expandReq.error)
+ : null;
+
+ const diskError = diskReq.error
+ ? diskReq.error instanceof Error
+ ? diskReq.error.message
+ : String(diskReq.error)
+ : null;
+
+ const vgError = volumeGroupReq.error
+ ? volumeGroupReq.error instanceof Error
+ ? volumeGroupReq.error.message
+ : String(volumeGroupReq.error)
+ : null;
+
+ const lvError = logicalVolumeReq.error
+ ? logicalVolumeReq.error instanceof Error
+ ? logicalVolumeReq.error.message
+ : String(logicalVolumeReq.error)
+ : null;
+
+ const canExpand = disk && volumeGroup && logicalVolume;
+
+ return (
+
+
+ Disk Expansion (LVM)
+
+ Expand your Dappnode filesystem with another storage device.{" "}
+
+
+ How-to guide
+
+
+
+
+ {/* Mode selection */}
+ {mode === "idle" && (
+
+
+
+
+
+ Automatic expansion
+
+
+
+
+ Extend the host filesystem?
+
+ Extending the filesystem is a dangerous operation that cannot be undone. You must read the DAppNode
+ documentation about extending the filesystem with LVM. Contact support if you have any doubt.
+
+
+
+ Cancel
+ cleanAndSetMode("automatic")}>
+ I understand, proceed
+
+
+
+
+
+
+
+ Manual expansion
+
+
+
+ Extend the host filesystem?
+
+ Extending the filesystem is a dangerous operation that cannot be undone. You must read the DAppNode
+ documentation about extending the filesystem with LVM. Contact support if you have any doubt.
+
+
+
+ Cancel
+ cleanAndSetMode("manual")}>I understand, proceed
+
+
+
+
+ )}
+
+ {/* Step 1: Select storage device (both modes) */}
+ {mode !== "idle" && (
+
+
+
+ Mode: {mode}
+
+
setMode("idle")}>
+ Cancel
+
+
+
+
+
1. Select storage device
+
+ {diskReq.loading ? : "Get storage devices"}
+
+ {diskReq.result && diskReq.result.length > 0 && (
+
+
+
+
+
+ {diskReq.result.map((d) => (
+
+ {d.name} ({d.size})
+
+ ))}
+
+
+ )}
+ {diskError &&
{diskError}
}
+
+
+ )}
+
+ {/* Automatic mode: get defaults */}
+ {mode === "automatic" && disk && (
+
+
2. Get DAppNode default values
+
+ Get default values
+
+
+ )}
+
+ {/* Manual mode: select VG */}
+ {mode === "manual" && disk && (
+
+
2. Select Volume Group
+
+ {volumeGroupReq.loading ? : "Get volume groups"}
+
+ {volumeGroupReq.result && volumeGroupReq.result.length > 0 && (
+
+
+
+
+
+ {volumeGroupReq.result.map((vg) => (
+
+ {vg.vg_name} ({vg.vg_size})
+
+ ))}
+
+
+ )}
+ {vgError &&
{vgError}
}
+
+ )}
+
+ {/* Manual mode: select LV */}
+ {mode === "manual" && volumeGroup && (
+
+
3. Select Logical Volume
+
+ {logicalVolumeReq.loading ? : "Get logical volumes"}
+
+ {logicalVolumeReq.result && logicalVolumeReq.result.length > 0 && (
+
+
+
+
+
+ {logicalVolumeReq.result.map((lv) => (
+
+ {lv.lv_name} ({lv.lv_size}) — {lv.vg_name}
+
+ ))}
+
+
+ )}
+ {lvError &&
{lvError}
}
+
+ )}
+
+ {/* Final step: expand */}
+ {mode !== "idle" && canExpand && (
+
+
{mode === "automatic" ? "3" : "4"}. Expand disk space
+
+
+ Storage device: {disk}
+
+
+ Volume Group: {volumeGroup}
+
+
+ Logical Volume: {logicalVolume}
+
+
+
+ {expandReq.loading ? : null}
+ Expand host filesystem
+
+
+ )}
+
+ {/* Result */}
+ {expandReq.result && (
+
+
+ {expandReq.result}
+
+ )}
+
+ {expandError && (
+
+
+ {expandError}
+
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/host/DockerUpgradeSection.tsx b/packages/admin-ui/src/pages-new/home/settings/host/DockerUpgradeSection.tsx
new file mode 100644
index 0000000000..5a24acd96f
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/host/DockerUpgradeSection.tsx
@@ -0,0 +1,140 @@
+import React, { useState, useEffect } from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { DockerUpgradeRequirements } from "@dappnode/types";
+import { lt } from "semver";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { CheckCircle2, XCircle, Loader2, HardDrive } from "lucide-react";
+
+export function DockerUpgradeSection() {
+ const [checkReq, setCheckReq] = useState<{
+ loading?: boolean;
+ result?: DockerUpgradeRequirements;
+ error?: unknown;
+ }>({});
+ const [canUpdate, setCanUpdate] = useState(false);
+
+ useEffect(() => {
+ if (checkReq.result) {
+ const {
+ isDockerInUnattendedUpgrades,
+ isDockerInstalledThroughApt,
+ dockerHostVersion,
+ dockerLatestVersion
+ } = checkReq.result;
+ setCanUpdate(
+ !isDockerInUnattendedUpgrades ||
+ !isDockerInstalledThroughApt ||
+ Boolean(dockerLatestVersion && lt(dockerHostVersion, dockerLatestVersion))
+ );
+ }
+ }, [checkReq.result]);
+
+ async function dockerUpdateCheck() {
+ try {
+ setCheckReq({ loading: true });
+ const requirements = await api.dockerUpgradeCheck();
+ setCheckReq({ result: requirements });
+ } catch (e) {
+ setCheckReq({ error: e });
+ }
+ }
+
+ const dockerCheckError = checkReq.error
+ ? `Error checking Docker: ${checkReq.error instanceof Error ? checkReq.error.message : String(checkReq.error)}`
+ : null;
+
+ async function dockerUpdate() {
+ const toastId = toast.loading("Updating Docker...");
+ try {
+ await api.dockerUpgrade();
+ toast.success("Docker updated successfully", { id: toastId });
+ await dockerUpdateCheck();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Docker
+ Check Docker installation status and update if needed.
+
+
+
+ {checkReq.loading ? (
+
+ ) : (
+
+ )}
+ Check Docker Status
+
+
+ {checkReq.result && (
+
+
+
+
+
+ )}
+
+ {dockerCheckError && {dockerCheckError}
}
+
+ {canUpdate && (
+
+
+ Update Docker
+
+
+
+ Docker update
+
+ Warning: the system may need to reboot. Make sure you can sustain some minutes of downtime.
+
+
+
+ Cancel
+ Update
+
+
+
+ )}
+
+
+ );
+}
+
+function DockerCheckItem({ label, ok }: { label: string; ok?: boolean }) {
+ return (
+
+ {ok === undefined ? (
+
+ ) : ok ? (
+
+ ) : (
+
+ )}
+ {label}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/host/HostPasswordSection.tsx b/packages/admin-ui/src/pages-new/home/settings/host/HostPasswordSection.tsx
new file mode 100644
index 0000000000..709ffea54b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/host/HostPasswordSection.tsx
@@ -0,0 +1,70 @@
+import React, { useState } from "react";
+import { useApi } from "api";
+import { validateStrongPasswordAsDockerEnv, validatePasswordsMatch } from "utils/validation";
+import { passwordChange } from "pages-new/utils/actions";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+
+export function HostPasswordSection() {
+ const passwordIsSecureReq = useApi.passwordIsSecure();
+ const [password, setPassword] = useState("");
+ const [password2, setPassword2] = useState("");
+
+ const passwordError = password ? validateStrongPasswordAsDockerEnv(password) : "";
+ const password2Error = password2 ? validatePasswordsMatch(password, password2) : "";
+ const isValid = password && password2 && !passwordError && !password2Error;
+
+ function onChangePassword() {
+ if (isValid) passwordChange(password);
+ }
+
+ const showInsecureWarning = passwordIsSecureReq.data === false;
+
+ return (
+
+
+ Host User Password
+
+ Change the host machine's user password. Store it safely — you will never see it again.
+
+
+
+ {showInsecureWarning && (
+
+ Action recommended: Your host user password is still the factory default and is insecure.
+ Change it to a strong password.
+
+ )}
+
+
New password
+
setPassword(e.target.value)}
+ aria-invalid={!!passwordError}
+ />
+ {passwordError &&
{passwordError}
}
+
+
+
Confirm new password
+
setPassword2(e.target.value)}
+ aria-invalid={!!password2Error}
+ />
+ {password2Error &&
{password2Error}
}
+
+
+ Change Host Password
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/host/HostTab.tsx b/packages/admin-ui/src/pages-new/home/settings/host/HostTab.tsx
new file mode 100644
index 0000000000..b0be460ef2
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/host/HostTab.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import { Separator } from "components/primitives/separator";
+import { SshSection } from "./SshSection";
+import { PowerManagementSection } from "./PowerManagementSection";
+import { HostPasswordSection } from "./HostPasswordSection";
+import { UpdateUpgradeSection } from "./UpdateUpgradeSection";
+import { DockerUpgradeSection } from "./DockerUpgradeSection";
+import { DiskExpansionSection } from "./DiskExpansionSection";
+
+export function HostTab() {
+ return (
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/host/PowerManagementSection.tsx b/packages/admin-ui/src/pages-new/home/settings/host/PowerManagementSection.tsx
new file mode 100644
index 0000000000..ea32169e4c
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/host/PowerManagementSection.tsx
@@ -0,0 +1,94 @@
+import React from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { Power, RotateCcw } from "lucide-react";
+
+export function PowerManagementSection() {
+ async function reboot() {
+ const toastId = toast.loading("Rebooting host...");
+ try {
+ await api.rebootHost();
+ toast.success("Reboot command sent", { id: toastId });
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ async function powerOff() {
+ const toastId = toast.loading("Powering off host...");
+ try {
+ await api.poweroffHost();
+ toast.success("Power off command sent", { id: toastId });
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Power Management
+
+ Only use these as a last resort when all other troubleshooting options have been exhausted.
+
+
+
+
+
+
+
+ Reboot
+
+
+
+
+ Reboot host
+
+ Are you sure you want to reboot the host machine? Only do this if it's strictly necessary.
+
+
+
+ Cancel
+ Reboot
+
+
+
+
+
+
+
+
+ Power Off
+
+
+
+
+ Power off host
+
+ WARNING! Your machine will power off and you will not be able to turn it back on without physical
+ access.
+
+
+
+ Cancel
+ Power Off
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/host/SshSection.tsx b/packages/admin-ui/src/pages-new/home/settings/host/SshSection.tsx
new file mode 100644
index 0000000000..2f83f07857
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/host/SshSection.tsx
@@ -0,0 +1,153 @@
+import React, { useState, useEffect } from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { ShhStatus } from "@dappnode/types";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Switch } from "components/primitives/switch";
+import { Separator } from "components/primitives/separator";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { Terminal, Loader2 } from "lucide-react";
+
+export function SshSection() {
+ const [sshStatus, setSshStatus] = useState<{ loading?: boolean; result?: ShhStatus; error?: unknown }>({});
+ const [sshPort, setSshPort] = useState("");
+ const [portFetched, setPortFetched] = useState(false);
+
+ useEffect(() => {
+ fetchSshStatus();
+ }, []);
+
+ async function fetchSshStatus() {
+ try {
+ setSshStatus({ loading: true });
+ const status = await api.sshStatusGet();
+ setSshStatus({ result: status });
+ } catch (e) {
+ setSshStatus({ error: e });
+ }
+ }
+
+ async function changeSshStatus(status: ShhStatus) {
+ const toastId = toast.loading(`${status === "enabled" ? "Enabling" : "Disabling"} SSH...`);
+ try {
+ await api.sshStatusSet({ status });
+ toast.success(`SSH ${status === "enabled" ? "enabled" : "disabled"}`, { id: toastId });
+ await fetchSshStatus();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ async function fetchSshPort() {
+ try {
+ const port = await api.sshPortGet();
+ setSshPort(String(port));
+ setPortFetched(true);
+ } catch (e) {
+ toast.error(`Error fetching port: ${e instanceof Error ? e.message : String(e)}`);
+ }
+ }
+
+ async function updateSshPort() {
+ const portNum = parseInt(sshPort, 10);
+ if (isNaN(portNum) || portNum <= 0 || portNum > 65535) {
+ toast.error("Port must be between 1 and 65535");
+ return;
+ }
+ const toastId = toast.loading("Changing SSH port...");
+ try {
+ await api.sshPortSet({ port: portNum });
+ toast.success("SSH port changed", { id: toastId });
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ const isEnabled = sshStatus.result === "enabled";
+
+ const sshErrorMsg = sshStatus.error
+ ? `Error: ${sshStatus.error instanceof Error ? sshStatus.error.message : String(sshStatus.error)}`
+ : null;
+
+ return (
+
+
+ SSH Access
+ Manage SSH access to your Dappnode host machine.
+
+
+
+
+
+ SSH Service
+ {sshStatus.loading && }
+
+ {sshStatus.result &&
+ !sshStatus.loading &&
+ (isEnabled ? (
+
+
+
+
+
+
+ Disable SSH
+
+ Warning: you will lose SSH access. Make sure you have an alternative way to access your Dappnode
+ before disabling SSH.
+
+
+
+ Cancel
+ changeSshStatus("disabled")}>Disable
+
+
+
+ ) : (
+
changeSshStatus("enabled")} />
+ ))}
+
+
+ {isEnabled && (
+ <>
+
+
+
SSH Port
+
+ setSshPort(e.target.value)}
+ className="tw:w-32"
+ />
+
+ Fetch Port
+
+
+ Change
+
+
+
+ >
+ )}
+
+ {sshErrorMsg && {sshErrorMsg}
}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/host/UpdateUpgradeSection.tsx b/packages/admin-ui/src/pages-new/home/settings/host/UpdateUpgradeSection.tsx
new file mode 100644
index 0000000000..1a3192ddc7
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/host/UpdateUpgradeSection.tsx
@@ -0,0 +1,62 @@
+import React from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { ArrowUpCircle } from "lucide-react";
+
+export function UpdateUpgradeSection() {
+ async function updateUpgrade() {
+ const toastId = toast.loading("Updating and upgrading...");
+ try {
+ await api.updateUpgrade();
+ toast.success("Updated and upgraded successfully", { id: toastId });
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Update & Upgrade Host
+
+ Update and upgrade the host machine's packages. This may require a reboot.
+
+
+
+
+
+
+
+ Update & Upgrade
+
+
+
+
+ Update and upgrade
+
+ This action might update Docker among other packages. You might lose connectivity temporarily.
+
+
+
+ Cancel
+ Proceed
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/host/index.ts b/packages/admin-ui/src/pages-new/home/settings/host/index.ts
new file mode 100644
index 0000000000..68a7dd457b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/host/index.ts
@@ -0,0 +1 @@
+export { HostTab } from "./HostTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/index.ts b/packages/admin-ui/src/pages-new/home/settings/index.ts
new file mode 100644
index 0000000000..fe55418b51
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/index.ts
@@ -0,0 +1,10 @@
+export { InfoTab } from "./info";
+export { WifiTab } from "./wifi";
+export { VpnTab } from "./vpn";
+export { UpdatesTab } from "./updates";
+export { AppTab } from "./app";
+export { ProfileTab } from "./profile";
+export { HostTab } from "./host";
+export { NetworkTab } from "./network";
+export { IpfsTab } from "./ipfs";
+export { AdvancedTab } from "./advanced";
diff --git a/packages/admin-ui/src/pages-new/home/settings/info/ActivitySection.tsx b/packages/admin-ui/src/pages-new/home/settings/info/ActivitySection.tsx
new file mode 100644
index 0000000000..b086791977
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/info/ActivitySection.tsx
@@ -0,0 +1,77 @@
+import React, { useState } from "react";
+import { useApi } from "api";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Badge } from "components/primitives/badge";
+import { Button } from "components/primitives/button";
+import { ChevronDown, ChevronUp, Download } from "lucide-react";
+import { UserActionLog } from "@dappnode/types";
+
+export function ActivitySection() {
+ const [showCount, setShowCount] = useState(20);
+ const userActionLogs = useApi.getUserActionLogs({ first: showCount });
+ const [expanded, setExpanded] = useState>({});
+
+ const logs: UserActionLog[] = userActionLogs.data || [];
+
+ return (
+
+
+ Activity Log
+ Recent actions performed on this Dappnode.
+
+
+ {logs.length === 0 && No activity logs available.
}
+ {logs.map((log, i) => {
+ const isExpanded = expanded[i] ?? false;
+ const hasArgs = log.args && log.args.length > 0;
+ const hasResult = log.result !== undefined && log.result !== null;
+ return (
+
+
setExpanded((prev) => ({ ...prev, [i]: !prev[i] }))}
+ >
+
+ {log.level || "info"}
+
+
+
{log.event}
+
+ {log.timestamp ? new Date(log.timestamp).toLocaleString() : ""}
+
+
+ {(hasArgs || hasResult) && (
+
+ {isExpanded ? : }
+
+ )}
+
+ {isExpanded && (
+
+
+ {JSON.stringify({ args: log.args, result: log.result }, null, 2)}
+
+
+ )}
+
+ );
+ })}
+ {logs.length >= showCount && (
+ setShowCount((c) => c + 20)} className="tw:w-full">
+ Load more
+
+ )}
+ {logs.length > 0 && (
+
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/info/AutoDiagnoseSection.tsx b/packages/admin-ui/src/pages-new/home/settings/info/AutoDiagnoseSection.tsx
new file mode 100644
index 0000000000..0e4173734d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/info/AutoDiagnoseSection.tsx
@@ -0,0 +1,94 @@
+import React, { useState } from "react";
+import { useApi } from "api";
+import { notEmpty } from "utils/typescript";
+import * as formatDiagnose from "pages-new/utils/autoDiagnoseTexts";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { CheckCircle2, XCircle, Loader2, ChevronDown, ChevronUp } from "lucide-react";
+
+/* ── Types ──────────────────────────────────────────────────────────── */
+
+type DiagnoseResult = {
+ loading?: boolean;
+ ok?: boolean;
+ msg: string;
+ solutions?: string[];
+ link?: { linkMsg: string; linkUrl: string };
+};
+
+/* ── DiagnoseItem ───────────────────────────────────────────────────── */
+
+function DiagnoseItem({ result }: { result: DiagnoseResult }) {
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+
+
setExpanded((x) => !x)}>
+ {result.loading ? (
+
+ ) : result.ok ? (
+
+ ) : (
+
+ )}
+ {result.msg}
+ {result.solutions && result.solutions.length > 0 && (
+
+ {expanded ? : }
+
+ )}
+
+ {expanded && result.solutions && (
+
+ )}
+
+ );
+}
+
+/* ── AutoDiagnoseSection ────────────────────────────────────────────── */
+
+export function AutoDiagnoseSection() {
+ const publicIpRes = useApi.ipPublicGet();
+ const systemInfo = useApi.systemInfoGet();
+ const hostStats = useApi.statsDiskGet();
+ const dnpInstalled = useApi.packagesGet();
+ const ipfsTest = useApi.ipfsTest();
+
+ const diagnosesArray: DiagnoseResult[] = [
+ formatDiagnose.ipfs(ipfsTest),
+ formatDiagnose.internetConnection(publicIpRes, systemInfo),
+ formatDiagnose.openPorts(systemInfo),
+ formatDiagnose.noNatLoopback(systemInfo),
+ formatDiagnose.diskSpace(hostStats),
+ formatDiagnose.coreDnpsRunning(dnpInstalled)
+ ].filter(notEmpty);
+
+ return (
+
+
+ Auto Diagnose
+ Automated check of your Dappnode health and connectivity.
+
+
+ {diagnosesArray.map((r, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/info/InfoTab.tsx b/packages/admin-ui/src/pages-new/home/settings/info/InfoTab.tsx
new file mode 100644
index 0000000000..89f0341fa5
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/info/InfoTab.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Separator } from "components/primitives/separator";
+import { AutoDiagnoseSection } from "./AutoDiagnoseSection";
+import { ReportSection } from "./ReportSection";
+import { ActivitySection } from "./ActivitySection";
+
+export function InfoTab() {
+ return (
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/info/ReportSection.tsx b/packages/admin-ui/src/pages-new/home/settings/info/ReportSection.tsx
new file mode 100644
index 0000000000..2c994314be
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/info/ReportSection.tsx
@@ -0,0 +1,48 @@
+import React, { useMemo } from "react";
+import { useApi } from "api";
+import { formatTopicBody, formatTopicUrl } from "pages-new/utils/discourseTopic";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { ExternalLink } from "lucide-react";
+
+export function ReportSection() {
+ const systemInfo = useApi.systemInfoGet();
+ const diagnose = useApi.diagnose();
+
+ const topicBody = useMemo(() => {
+ const versions = systemInfo.data?.versionData
+ ? Object.entries(systemInfo.data.versionData).map(([name, version]) => ({
+ name,
+ version: version || "unknown"
+ }))
+ : [];
+ const hostDiagnose = diagnose.data || [];
+ return formatTopicBody(versions, hostDiagnose);
+ }, [systemInfo.data, diagnose.data]);
+
+ const topicUrl = formatTopicUrl(topicBody);
+
+ return (
+
+
+ Support Report
+
+ Generate a report with your system information to share with Dappnode support.
+
+
+
+
+
{topicBody || "Loading report data..."}
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/info/index.ts b/packages/admin-ui/src/pages-new/home/settings/info/index.ts
new file mode 100644
index 0000000000..e3d3a8766b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/info/index.ts
@@ -0,0 +1 @@
+export { InfoTab } from "./InfoTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsClientSection.tsx b/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsClientSection.tsx
new file mode 100644
index 0000000000..0daaa32e74
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsClientSection.tsx
@@ -0,0 +1,154 @@
+import React, { useState, useEffect } from "react";
+import { useApi, api } from "api";
+import { toast } from "sonner";
+import { IpfsClientTarget } from "@dappnode/types";
+import { continueIfCalleDisconnected } from "api/utils";
+import { ipfsDnpName } from "params";
+import { prettyDnpName } from "utils/format";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Skeleton } from "components/primitives/skeleton";
+import { Cloud, HardDrive } from "lucide-react";
+
+export function IpfsClientSection() {
+ const ipfsRepository = useApi.ipfsClientTargetGet();
+ const packagesReq = useApi.packagesGet();
+ const [clientTarget, setClientTarget] = useState(null);
+ const [gatewayTarget, setGatewayTarget] = useState("");
+
+ const isIpfsInstalled = packagesReq.data?.some((p) => p.dnpName === ipfsDnpName) ?? false;
+
+ useEffect(() => {
+ if (ipfsRepository.data) {
+ setClientTarget(ipfsRepository.data.ipfsClientTarget);
+ setGatewayTarget(ipfsRepository.data.ipfsGateway || "");
+ }
+ }, [ipfsRepository.data]);
+
+ async function changeIpfsClient() {
+ if (!clientTarget) return;
+
+ const switchingFromRemoteToLocal =
+ ipfsRepository.data?.ipfsClientTarget === IpfsClientTarget.remote && clientTarget === IpfsClientTarget.local;
+
+ // If switching to local but IPFS not installed, install it first
+ if (switchingFromRemoteToLocal && !isIpfsInstalled) {
+ const installToastId = toast.loading(`Installing ${prettyDnpName(ipfsDnpName)}...`);
+ try {
+ await continueIfCalleDisconnected(
+ () =>
+ api.packageInstall({
+ name: ipfsDnpName,
+ options: {
+ BYPASS_CORE_RESTRICTION: true,
+ BYPASS_SIGNED_RESTRICTION: true
+ }
+ }),
+ ipfsDnpName
+ )();
+ toast.success(`Installed ${prettyDnpName(ipfsDnpName)}`, { id: installToastId });
+ packagesReq.revalidate();
+ } catch (e) {
+ toast.error(`Install failed: ${e instanceof Error ? e.message : String(e)}`, { id: installToastId });
+ return;
+ }
+ }
+
+ const toastId = toast.loading(`Setting IPFS mode to ${clientTarget}...`);
+ try {
+ await api.ipfsClientTargetSet({
+ ipfsRepository: {
+ ipfsClientTarget: clientTarget,
+ ipfsGateway: gatewayTarget
+ }
+ });
+ toast.success(`IPFS mode changed to ${clientTarget}`, { id: toastId });
+ ipfsRepository.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ if (!ipfsRepository.data) {
+ return (
+
+
+ IPFS Node
+
+
+
+
+
+ );
+ }
+
+ const isUnchanged =
+ ipfsRepository.data.ipfsClientTarget === clientTarget && ipfsRepository.data.ipfsGateway === gatewayTarget;
+
+ return (
+
+
+ IPFS Node
+
+ Dappnode uses IPFS to distribute packages in a decentralized way. Choose between a remote gateway or your own
+ local IPFS node.
+
+
+
+
+
setClientTarget(IpfsClientTarget.remote)}
+ className={`tw:flex tw:items-start tw:gap-3 tw:rounded-lg tw:border tw:p-4 tw:text-left tw:bg-background tw:transition-colors ${
+ clientTarget === IpfsClientTarget.remote
+ ? "tw:border-primary tw:bg-primary/5"
+ : "tw:border-border tw:hover:bg-muted/50"
+ }`}
+ >
+
+
+
Remote
+
+ Use a remote IPFS gateway (recommended for beginners)
+
+
+
+
setClientTarget(IpfsClientTarget.local)}
+ className={`tw:flex tw:items-start tw:gap-3 tw:rounded-lg tw:border tw:p-4 tw:text-left tw:bg-background tw:transition-colors ${
+ clientTarget === IpfsClientTarget.local
+ ? "tw:border-primary tw:bg-primary/5"
+ : "tw:border-border tw:hover:bg-muted/50"
+ }`}
+ >
+
+
+
Local
+
+ Use your own local IPFS node
+ {!isIpfsInstalled && " (will be installed)"}
+
+
+
+
+
+ {clientTarget === IpfsClientTarget.remote && (
+
+ Gateway URL
+ setGatewayTarget(e.target.value)}
+ />
+
+ )}
+
+
+ Apply Changes
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsPeersSection.tsx b/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsPeersSection.tsx
new file mode 100644
index 0000000000..c4aa394c1d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsPeersSection.tsx
@@ -0,0 +1,65 @@
+import React, { useState } from "react";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+
+export function IpfsPeersSection() {
+ const [peerInput, setPeerInput] = useState("");
+ const [status, setStatus] = useState<{ msg?: string; ok?: boolean; loading?: boolean }>({});
+
+ async function addPeer() {
+ if (!peerInput) return;
+ try {
+ setStatus({ loading: true, msg: "Adding peer..." });
+ // Use the IPFS API directly
+ const addRes = await fetch(
+ `http://ipfs.dappnode:5001/api/v0/swarm/connect?arg=${encodeURIComponent(peerInput)}`,
+ {
+ method: "POST"
+ }
+ );
+ if (!addRes.ok) throw new Error("Failed to connect to peer");
+
+ await fetch(`http://ipfs.dappnode:5001/api/v0/bootstrap/add?arg=${encodeURIComponent(peerInput)}`, {
+ method: "POST"
+ });
+
+ setStatus({ ok: true, msg: "Peer added successfully" });
+ } catch (e) {
+ setStatus({ ok: false, msg: `Failed: ${e instanceof Error ? e.message : String(e)}` });
+ }
+ }
+
+ return (
+
+
+ IPFS Peers
+ Connect to another Dappnode's IPFS node to peer-share content.
+
+
+
+ Peer multiaddress
+ setPeerInput(e.target.value)}
+ />
+
+
+ {status.loading ? "Adding..." : "Add Peer"}
+
+ {status.msg && (
+
+ {status.msg}
+
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsTab.tsx b/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsTab.tsx
new file mode 100644
index 0000000000..d7d1578353
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/ipfs/IpfsTab.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import { Separator } from "components/primitives/separator";
+import { IpfsClientSection } from "./IpfsClientSection";
+import { IpfsPeersSection } from "./IpfsPeersSection";
+
+export function IpfsTab() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/ipfs/index.ts b/packages/admin-ui/src/pages-new/home/settings/ipfs/index.ts
new file mode 100644
index 0000000000..89757cf8fb
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/ipfs/index.ts
@@ -0,0 +1 @@
+export { IpfsTab } from "./IpfsTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/network/NetworkTab.tsx b/packages/admin-ui/src/pages-new/home/settings/network/NetworkTab.tsx
new file mode 100644
index 0000000000..fb1682e4ed
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/network/NetworkTab.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import { Separator } from "components/primitives/separator";
+import { StaticIpSection } from "./StaticIpSection";
+import { PortsSection } from "./PortsSection";
+
+export function NetworkTab() {
+ return (
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/network/PortsSection.tsx b/packages/admin-ui/src/pages-new/home/settings/network/PortsSection.tsx
new file mode 100644
index 0000000000..cab6ef38cd
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/network/PortsSection.tsx
@@ -0,0 +1,184 @@
+import React, { useState } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { prettyDnpName } from "utils/format";
+import { ApiTablePortStatus, UpnpTablePortStatus } from "@dappnode/types";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Label } from "components/primitives/label";
+import { Switch } from "components/primitives/switch";
+import { Badge } from "components/primitives/badge";
+import { CheckCircle2, XCircle, Loader2, Globe, Wifi } from "lucide-react";
+
+export function PortsSection() {
+ const systemInfo = useApi.systemInfoGet();
+ const portsToOpen = useApi.portsToOpenGet();
+ const natRenewalStatus = useApi.natRenewalIsEnabled();
+ const [apiReqStatus, setApiReqStatus] = useState<{
+ loading?: boolean;
+ result?: ApiTablePortStatus[];
+ error?: unknown;
+ }>({});
+ const [upnpReqStatus, setUpnpReqStatus] = useState<{
+ loading?: boolean;
+ result?: UpnpTablePortStatus[];
+ error?: unknown;
+ }>({});
+
+ async function apiStatusGet() {
+ if (!portsToOpen.data) return;
+ try {
+ setApiReqStatus({ loading: true });
+ const apiPorts = await api.portsApiStatusGet({ portsToOpen: portsToOpen.data });
+ setApiReqStatus({ result: apiPorts });
+ } catch (e) {
+ setApiReqStatus({ error: e });
+ }
+ }
+
+ async function upnpStatusGet() {
+ if (!portsToOpen.data) return;
+ try {
+ setUpnpReqStatus({ loading: true });
+ const upnpPorts = await api.portsUpnpStatusGet({ portsToOpen: portsToOpen.data });
+ setUpnpReqStatus({ result: upnpPorts });
+ } catch (e) {
+ setUpnpReqStatus({ error: e });
+ }
+ }
+
+ async function onUpnpSwitchToggle(checked: boolean) {
+ const toastId = toast.loading("Refreshing UPnP port mapping...");
+ try {
+ await api.natRenewalEnable({ enableNatRenewal: checked });
+ toast.success("Successfully updated UPnP setting", { id: toastId });
+ natRenewalStatus.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ const isUpnpEnabled = systemInfo.data?.upnpAvailable ?? false;
+ const ports = portsToOpen.data || [];
+
+ return (
+
+
+ Ports
+ Monitor and manage network ports required by Dappnode.
+
+
+ {systemInfo.data && (
+
+ {systemInfo.data.publicIp !== systemInfo.data.internalIp ? (
+ isUpnpEnabled ? (
+ <>
+
+ UPnP is enabled
+ >
+ ) : (
+ <>
+
+ UPnP is disabled — open ports manually or enable UPnP on your router
+ >
+ )
+ ) : (
+ <>
+
+ Public and local IPs match — no port forwarding needed
+ >
+ )}
+
+ )}
+
+ {/* NAT Renewal toggle */}
+ {natRenewalStatus.data !== undefined && (
+
+ NAT Renewal (UPnP auto-mapping)
+
+
+ )}
+
+
+
+ {apiReqStatus.loading ? (
+
+ ) : (
+
+ )}
+ Scan API ports
+
+ {isUpnpEnabled && (
+
+ {upnpReqStatus.loading ? (
+
+ ) : (
+
+ )}
+ Scan UPnP ports
+
+ )}
+
+
+ {/* Ports table */}
+ {ports.length > 0 && (
+
+
+
+
+ Port
+ Protocol
+ Service
+ {apiReqStatus.result && API }
+ {upnpReqStatus.result && UPnP }
+
+
+
+ {ports.map((port, i) => {
+ const apiMatch = apiReqStatus.result?.find((p) => p.port === port.portNumber);
+ const upnpMatch = upnpReqStatus.result?.find((p) => p.port === port.portNumber);
+ return (
+
+ {port.portNumber}
+
+ {port.protocol}
+
+ {prettyDnpName(port.serviceName)}
+ {apiReqStatus.result && (
+
+
+
+ )}
+ {upnpReqStatus.result && (
+
+
+
+ )}
+
+ );
+ })}
+
+
+
+ )}
+
+
+ );
+}
+
+function PortStatusBadge({ status }: { status?: string }) {
+ if (!status || status === "unknown") return Unknown ;
+ if (status === "open") return Open ;
+ if (status === "closed") return Closed ;
+ return {status} ;
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/network/StaticIpSection.tsx b/packages/admin-ui/src/pages-new/home/settings/network/StaticIpSection.tsx
new file mode 100644
index 0000000000..76254cd0d3
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/network/StaticIpSection.tsx
@@ -0,0 +1,64 @@
+import React, { useState, useEffect } from "react";
+import { api } from "api";
+import { toast } from "sonner";
+import { useSelector } from "react-redux";
+import { getStaticIp } from "services/dappnodeStatus/selectors";
+import isIpv4 from "utils/isIpv4";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+
+export function StaticIpSection() {
+ const staticIp = useSelector(getStaticIp);
+ const [input, setInput] = useState(staticIp || "");
+
+ useEffect(() => {
+ setInput(staticIp || "");
+ }, [staticIp]);
+
+ async function updateStaticIp(newStaticIp: string) {
+ const toastId = toast.loading("Setting static IP...");
+ try {
+ await api.setStaticIp({ staticIp: newStaticIp });
+ toast.success(newStaticIp ? "Static IP set" : "Static IP disabled", { id: toastId });
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Static IP
+
+ Set a static IP for your Dappnode. If you have a static IP, enable it here for proper VPN and connectivity.
+
+
+
+
+ IP Address
+ setInput(e.target.value)}
+ />
+
+
+ updateStaticIp(input)}
+ >
+ {staticIp ? "Update" : "Enable"}
+
+ {staticIp && (
+ updateStaticIp("")}>
+ Disable
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/network/index.ts b/packages/admin-ui/src/pages-new/home/settings/network/index.ts
new file mode 100644
index 0000000000..e380e27dd1
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/network/index.ts
@@ -0,0 +1 @@
+export { NetworkTab } from "./NetworkTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/profile/ChangeDappnodeNameSection.tsx b/packages/admin-ui/src/pages-new/home/settings/profile/ChangeDappnodeNameSection.tsx
new file mode 100644
index 0000000000..f0a0e10ad0
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/profile/ChangeDappnodeNameSection.tsx
@@ -0,0 +1,49 @@
+import React, { useEffect, useState } from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { api } from "api";
+import { toast } from "sonner";
+import { getDappnodeName } from "services/dappnodeStatus/selectors";
+import { fetchSystemInfo } from "services/dappnodeStatus/actions";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+
+export function ChangeDappnodeNameSection() {
+ const dappnodeWebName = useSelector(getDappnodeName);
+ const [input, setInput] = useState(dappnodeWebName);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ setInput(dappnodeWebName);
+ }, [dappnodeWebName]);
+
+ async function onChangeName() {
+ const toastId = toast.loading("Setting Dappnode name...");
+ try {
+ await api.dappnodeWebNameSet({ dappnodeWebName: input });
+ toast.success("Dappnode name changed successfully", { id: toastId });
+ dispatch(fetchSystemInfo());
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Dappnode Name
+ Customize the display name of your Dappnode.
+
+
+
+ Name
+ setInput(e.target.value)} />
+
+
+ Change Name
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/profile/ChangePasswordSection.tsx b/packages/admin-ui/src/pages-new/home/settings/profile/ChangePasswordSection.tsx
new file mode 100644
index 0000000000..a4a8b1fe6d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/profile/ChangePasswordSection.tsx
@@ -0,0 +1,78 @@
+import React, { useState } from "react";
+import { apiAuth } from "api";
+import { toast } from "sonner";
+import { validateStrongPassword, validatePasswordsMatch } from "utils/validation";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+
+export function ChangePasswordSection() {
+ const [oldPassword, setOldPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [newPassword2, setNewPassword2] = useState("");
+
+ const passwordError = newPassword ? validateStrongPassword(newPassword) : "";
+ const password2Error = newPassword2 ? validatePasswordsMatch(newPassword, newPassword2) : "";
+ const isValid = oldPassword && newPassword && newPassword2 && !passwordError && !password2Error;
+
+ async function onChangePassword() {
+ if (!isValid) return;
+ const toastId = toast.loading("Changing password...");
+ try {
+ await apiAuth.changePass({ password: oldPassword, newPassword });
+ toast.success("Password changed successfully. Please log in again.", { id: toastId });
+ await apiAuth.logoutAndReload();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Change UI Password
+ Change the password used to access the Dappnode admin UI.
+
+
+
+ Current password
+ setOldPassword(e.target.value)}
+ />
+
+
+
New password
+
setNewPassword(e.target.value)}
+ aria-invalid={!!passwordError}
+ />
+ {passwordError &&
{passwordError}
}
+
+
+
Confirm new password
+
setNewPassword2(e.target.value)}
+ aria-invalid={!!password2Error}
+ />
+ {password2Error &&
{password2Error}
}
+
+
+ Change Password
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/profile/ProfileTab.tsx b/packages/admin-ui/src/pages-new/home/settings/profile/ProfileTab.tsx
new file mode 100644
index 0000000000..0a145c1d52
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/profile/ProfileTab.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import { Separator } from "components/primitives/separator";
+import { ChangePasswordSection } from "./ChangePasswordSection";
+import { ChangeDappnodeNameSection } from "./ChangeDappnodeNameSection";
+
+export function ProfileTab() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/profile/index.ts b/packages/admin-ui/src/pages-new/home/settings/profile/index.ts
new file mode 100644
index 0000000000..26cd26de49
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/profile/index.ts
@@ -0,0 +1 @@
+export { ProfileTab } from "./ProfileTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/updates/AutoUpdatesSection.tsx b/packages/admin-ui/src/pages-new/home/settings/updates/AutoUpdatesSection.tsx
new file mode 100644
index 0000000000..646ca83071
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/updates/AutoUpdatesSection.tsx
@@ -0,0 +1,71 @@
+import React from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { prettyDnpName } from "utils/format";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Switch } from "components/primitives/switch";
+import { Label } from "components/primitives/label";
+import { Skeleton } from "components/primitives/skeleton";
+
+export function AutoUpdatesSection() {
+ const autoUpdateDataReq = useApi.autoUpdateDataGet();
+
+ async function setUpdateSettings(id: string, enabled: boolean) {
+ const prettyName = prettyDnpName(id);
+ const actioning = enabled ? "Enabling" : "Disabling";
+ const actioned = enabled ? "Enabled" : "Disabled";
+ const toastId = toast.loading(`${actioning} auto updates for ${prettyName}...`);
+ try {
+ await api.autoUpdateSettingsEdit({ id, enabled });
+ toast.success(`${actioned} auto updates for ${prettyName}`, { id: toastId });
+ autoUpdateDataReq.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ if (autoUpdateDataReq.isValidating && !autoUpdateDataReq.data) {
+ return (
+
+
+ Auto Updates
+
+
+
+
+
+
+ );
+ }
+
+ const dnpsToShow = autoUpdateDataReq.data?.dnpsToShow || [];
+
+ return (
+
+
+ Auto Updates
+
+ Enable auto-updates to install the latest versions automatically. Major breaking updates always require your
+ approval.
+
+
+
+ {dnpsToShow.map(({ id, displayName, enabled }) => (
+
+
+ {displayName}
+
+ setUpdateSettings(id, checked)}
+ />
+
+ ))}
+ {dnpsToShow.length === 0 && (
+ No packages available for auto-updates.
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/updates/SystemUpdateSection.tsx b/packages/admin-ui/src/pages-new/home/settings/updates/SystemUpdateSection.tsx
new file mode 100644
index 0000000000..c7b69fd13e
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/updates/SystemUpdateSection.tsx
@@ -0,0 +1,111 @@
+import React from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { prettyDnpName } from "utils/format";
+import { getCoreUpdateAvailable, getCoreRequestStatus, getCoreUpdateData } from "services/coreUpdate/selectors";
+import { updateCore } from "services/coreUpdate/actions";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Skeleton } from "components/primitives/skeleton";
+import { CheckCircle2, AlertTriangle, ArrowRight } from "lucide-react";
+
+export function SystemUpdateSection() {
+ const dispatch = useDispatch();
+ const { loading, error } = useSelector(getCoreRequestStatus);
+ const coreUpdateAvailable = useSelector(getCoreUpdateAvailable);
+ const coreUpdateData = useSelector(getCoreUpdateData);
+
+ if (loading) {
+ return (
+
+
+ System Update
+
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ System Update
+
+
+
+
+
Error checking core version: {error}
+
+
+
+ );
+ }
+
+ if (!coreUpdateAvailable || !coreUpdateData || !coreUpdateData.available) {
+ return (
+
+
+ System Update
+
+
+
+
+ System is up to date
+
+
+
+ );
+ }
+
+ const { changelog, updateAlerts, packages: corePackages } = coreUpdateData;
+ const coreDeps = corePackages.filter((dnp) => !(dnp.name || "").includes("core"));
+
+ return (
+
+
+ System Update Available
+ A new version of Dappnode core is available.
+
+
+ {changelog && (
+
+ )}
+
+ {updateAlerts.map(({ from, to, message }) => (
+
+ {updateAlerts.length > 1 && (
+
+ )}
+
{message}
+
+ ))}
+
+ {coreDeps.length > 0 && (
+
+
Packages to update
+ {coreDeps.map((dep) => (
+
+ {prettyDnpName(dep.name)}
+ {dep.to}
+
+ ))}
+
+ )}
+
+ dispatch(updateCore())}>Update System
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/updates/UpdatesTab.tsx b/packages/admin-ui/src/pages-new/home/settings/updates/UpdatesTab.tsx
new file mode 100644
index 0000000000..32bbe45a0c
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/updates/UpdatesTab.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import { Separator } from "components/primitives/separator";
+import { SystemUpdateSection } from "./SystemUpdateSection";
+import { AutoUpdatesSection } from "./AutoUpdatesSection";
+
+export function UpdatesTab() {
+ return (
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/updates/index.ts b/packages/admin-ui/src/pages-new/home/settings/updates/index.ts
new file mode 100644
index 0000000000..14f1ac26b8
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/updates/index.ts
@@ -0,0 +1 @@
+export { UpdatesTab } from "./UpdatesTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/TailscaleSection.tsx b/packages/admin-ui/src/pages-new/home/settings/vpn/TailscaleSection.tsx
new file mode 100644
index 0000000000..7e4a5aa0b1
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/TailscaleSection.tsx
@@ -0,0 +1,159 @@
+import React, { useState, useEffect } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { tailscaleDnpName, docsUrl } from "params";
+import { prettyDnpName } from "utils/format";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Skeleton } from "components/primitives/skeleton";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import { Info, ArrowLeft } from "lucide-react";
+import { useNavigate } from "react-router-dom";
+import { UserSettingsAllDnps, UserSettings, PackageEnvs, SetupWizard as SetupWizardType } from "@dappnode/types";
+import { SetupWizard } from "components/SetupWizard";
+import { difference } from "utils/lodashExtended";
+
+export function TailscaleSection() {
+ const navigate = useNavigate();
+ const dnpRequest = useApi.packageGet({ dnpName: tailscaleDnpName });
+ const dnp = dnpRequest.data;
+
+ // Loading
+ if (dnpRequest.isValidating && !dnp) {
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+ Tailscale
+
+
+
+
+
+
+ );
+ }
+
+ // Not installed
+ if (!dnp) {
+ const notFound = dnpRequest.error?.message?.includes("No DNP was found");
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+ Tailscale
+
+
+ {notFound ? (
+
+
+
+ {prettyDnpName(tailscaleDnpName)} is not installed. Install it from the Packages section.
+
+
+ ) : (
+
+ {dnpRequest.error?.message || "Error loading Tailscale package"}
+
+ )}
+
+
+
+ );
+ }
+
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+ );
+}
+
+/* ── Tailscale Config (SetupWizard) ─────────────────────────────────── */
+
+function TailscaleConfig({
+ dnp
+}: {
+ dnp: { dnpName: string; userSettings?: UserSettings; setupWizard?: SetupWizardType };
+}) {
+ const [localUserSettings, setLocalUserSettings] = useState({});
+
+ useEffect(() => {
+ if (dnp.userSettings) setLocalUserSettings({ [dnp.dnpName]: dnp.userSettings });
+ }, [dnp.userSettings, dnp.dnpName]);
+
+ function onSubmit(newUserSettings: UserSettingsAllDnps) {
+ setLocalUserSettings(newUserSettings);
+
+ const prevEnvs = dnp.userSettings?.environment || {};
+ const newEnvs = newUserSettings[dnp.dnpName].environment;
+ if (!newEnvs) return console.error("SetupWizard returned no ENVs");
+ const diffEnvs = difference(prevEnvs, newEnvs);
+
+ const serviceEnvs: PackageEnvs = Object.values(diffEnvs)[0];
+ const niceNames = Object.keys(serviceEnvs).map((name) => {
+ for (const field of dnp.setupWizard?.fields || [])
+ if (field.target?.type === "environment" && field.target.name === name) return field.title || name;
+ return name;
+ });
+
+ const envByService: Record> = {};
+ for (const [service, envs] of Object.entries(diffEnvs)) {
+ envByService[service] = envs;
+ }
+
+ toast.promise(
+ api.packageSetEnvironment({
+ dnpName: dnp.dnpName,
+ environmentByService: envByService
+ }),
+ {
+ loading: `Updating ${niceNames.join(", ")}...`,
+ success: "Tailscale configuration updated",
+ error: (e) => `Error: ${e instanceof Error ? e.message : String(e)}`
+ }
+ );
+ }
+
+ return (
+
+
+ Tailscale Configuration
+
+ Configure your Tailscale VPN connection.{" "}
+
+ Setup guide
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/VpnServicesList.tsx b/packages/admin-ui/src/pages-new/home/settings/vpn/VpnServicesList.tsx
new file mode 100644
index 0000000000..f259a46951
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/VpnServicesList.tsx
@@ -0,0 +1,127 @@
+import React, { useMemo } from "react";
+import { useApi } from "api";
+import { useNavigate } from "react-router-dom";
+import { vpnDnpName, wireguardDnpName, tailscaleDnpName, docsUrl } from "params";
+import { prettyDnpName } from "utils/format";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Badge } from "components/primitives/badge";
+import { Button } from "components/primitives/button";
+import { Skeleton } from "components/primitives/skeleton";
+import { ExternalLink, Shield, ChevronRight } from "lucide-react";
+
+/* ── Types ──────────────────────────────────────────────────────────── */
+
+interface VpnServiceInfo {
+ name: string;
+ dnpName: string;
+ installed: boolean;
+ running: boolean;
+ /** Route within the VPN tab (relative to /settings/vpn/) */
+ route: string;
+}
+
+/* ── Service card ───────────────────────────────────────────────────── */
+
+function VpnServiceCard({ service, onNavigate }: { service: VpnServiceInfo; onNavigate: (route: string) => void }) {
+ return (
+ service.installed && onNavigate(service.route)}
+ role={service.installed ? "button" : undefined}
+ >
+
+
+
+
+
{service.name}
+ {service.installed ? (
+
+ {service.running ? "Running" : "Stopped"}
+
+ ) : (
+
Not installed
+ )}
+
+
{prettyDnpName(service.dnpName)}
+
+
+ {service.installed &&
}
+
+ );
+}
+
+/* ── Main list ──────────────────────────────────────────────────────── */
+
+export function VpnServicesList() {
+ const navigate = useNavigate();
+ const dnpsRequest = useApi.packagesGet();
+
+ const vpnServices: VpnServiceInfo[] = useMemo(() => {
+ const dnps = dnpsRequest.data || [];
+ const getDnp = (name: string) => dnps.find((d) => d.dnpName === name);
+
+ const services: VpnServiceInfo[] = [
+ {
+ name: "Tailscale",
+ dnpName: tailscaleDnpName,
+ installed: !!getDnp(tailscaleDnpName),
+ running: getDnp(tailscaleDnpName)?.containers.some((c) => c.running) ?? false,
+ route: "tailscale"
+ },
+ {
+ name: "Wireguard",
+ dnpName: wireguardDnpName,
+ installed: !!getDnp(wireguardDnpName),
+ running: getDnp(wireguardDnpName)?.containers.some((c) => c.running) ?? false,
+ route: "wireguard"
+ },
+ {
+ name: "OpenVPN",
+ dnpName: vpnDnpName,
+ installed: !!getDnp(vpnDnpName),
+ running: getDnp(vpnDnpName)?.containers.some((c) => c.running) ?? false,
+ route: "openvpn"
+ }
+ ];
+
+ return services.sort((a, b) => (a.installed && !b.installed ? -1 : !a.installed && b.installed ? 1 : 0));
+ }, [dnpsRequest.data]);
+
+ if (dnpsRequest.isValidating && !dnpsRequest.data) {
+ return (
+
+
+ VPN Services
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ VPN Services
+
+ VPN services available on your Dappnode. Click on an installed service to manage its devices.
+
+
+
+ {vpnServices.map((service) => (
+ navigate(route)} />
+ ))}
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/VpnTab.tsx b/packages/admin-ui/src/pages-new/home/settings/vpn/VpnTab.tsx
new file mode 100644
index 0000000000..07473827e5
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/VpnTab.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { Routes, Route, Navigate } from "react-router-dom";
+import { VpnServicesList } from "./VpnServicesList";
+import { TailscaleSection } from "./TailscaleSection";
+import { OpenVpnDevicesHome } from "./openvpn/OpenVpnDevicesHome";
+import { OpenVpnDeviceDetails } from "./openvpn/OpenVpnDeviceDetails";
+import { WireguardDevicesHome } from "./wireguard/WireguardDevicesHome";
+import { WireguardDeviceDetails } from "./wireguard/WireguardDeviceDetails";
+
+/**
+ * VPN Tab — shows a list of VPN services with nested routes for
+ * device management (Tailscale config, OpenVPN devices, Wireguard devices).
+ */
+export function VpnTab() {
+ return (
+
+ {/* Default: service list overview */}
+ } />
+
+ {/* Tailscale */}
+ } />
+
+ {/* OpenVPN */}
+ } />
+ } />
+
+ {/* Wireguard */}
+ } />
+ } />
+
+ {/* Fallback */}
+ } />
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/helpers.ts b/packages/admin-ui/src/pages-new/home/settings/vpn/helpers.ts
new file mode 100644
index 0000000000..37508a0a2b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/helpers.ts
@@ -0,0 +1,13 @@
+const MAX_ID_LENGTH = 80;
+
+/**
+ * Coerce a VPN device name to contain only alphanumeric characters
+ * and stay within the max length limit.
+ */
+export function coerceDeviceName(name = ""): string {
+ name = name.replace(/\W/g, "");
+ if (name.length > MAX_ID_LENGTH) name = name.slice(0, MAX_ID_LENGTH);
+ return name;
+}
+
+export { MAX_ID_LENGTH };
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/index.ts b/packages/admin-ui/src/pages-new/home/settings/vpn/index.ts
new file mode 100644
index 0000000000..3766bdc060
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/index.ts
@@ -0,0 +1 @@
+export { VpnTab } from "./VpnTab";
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/openvpn/OpenVpnDeviceDetails.tsx b/packages/admin-ui/src/pages-new/home/settings/vpn/openvpn/OpenVpnDeviceDetails.tsx
new file mode 100644
index 0000000000..f939bb80c0
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/openvpn/OpenVpnDeviceDetails.tsx
@@ -0,0 +1,187 @@
+import React, { useState, useCallback } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { useApi } from "api";
+import ClipboardJS from "clipboard";
+import QrCode from "components/QrCode";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Skeleton } from "components/primitives/skeleton";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import { ArrowLeft, Copy, ExternalLink, QrCode as QrCodeIcon, TriangleAlert } from "lucide-react";
+import { VpnDeviceCredentials } from "@dappnode/types";
+
+export function OpenVpnDeviceDetails() {
+ const navigate = useNavigate();
+ const params = useParams();
+ const id = params.id || "";
+ const deviceCredentials = useApi.deviceCredentialsGet({ id });
+
+ // Loading
+ if (deviceCredentials.isValidating && !deviceCredentials.data) {
+ return (
+
+
navigate("..")}>
+
+ Back to devices
+
+
+
+ {id}
+
+
+
+
+
+
+ );
+ }
+
+ // Error
+ if (deviceCredentials.error) {
+ return (
+
+
navigate("..")}>
+
+ Back to devices
+
+
+
+ {id}
+
+
+ {deviceCredentials.error.message}
+
+
+
+ );
+ }
+
+ if (!deviceCredentials.data) return null;
+
+ return (
+
+
navigate("..")}>
+
+ Back to devices
+
+
+
+ );
+}
+
+/* ── Loaded state ───────────────────────────────────────────────────── */
+
+function OpenVpnDeviceDetailsLoaded({ device }: { device: VpnDeviceCredentials }) {
+ const [showQr, setShowQr] = useState(false);
+ const { id, url } = device;
+
+ // Clipboard setup
+ const clipboardRef = useCallback((node: HTMLDivElement | null) => {
+ if (node) {
+ new ClipboardJS(".openvpn-copy-btn", { container: node });
+ }
+ }, []);
+
+ return (
+
+
+
+ {id || "Device not found"}
+ OpenVPN credentials and connection details.
+
+
+ {/* VPN credentials URL */}
+
+
VPN credentials URL
+
+
+
+ {/* QR code toggle */}
+ setShowQr(!showQr)}>
+
+ {showQr ? "Hide" : "Show"} QR code
+
+ {showQr && url && (
+
+
+
+ )}
+
+ {/* Admin credentials */}
+ {device.admin && (
+ <>
+ {device.hasChangedPassword ? (
+
+
+ This admin user has already changed the password. Only the initial auto-generated password is
+ visible.
+
+
+ ) : (
+ <>
+
+
+
Admin password
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ )}
+
+ {/* Security warning */}
+
+
+
+ Beware of shoulder surfing attacks (unsolicited observers). This data grants admin access to your
+ DAppNode.
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/openvpn/OpenVpnDevicesHome.tsx b/packages/admin-ui/src/pages-new/home/settings/vpn/openvpn/OpenVpnDevicesHome.tsx
new file mode 100644
index 0000000000..c1b6db9b35
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/openvpn/OpenVpnDevicesHome.tsx
@@ -0,0 +1,279 @@
+import React, { useState } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { useNavigate } from "react-router-dom";
+import { vpnDnpName, MAIN_ADMIN_NAME, docsUrl } from "params";
+import { prettyDnpName } from "utils/format";
+import { coerceDeviceName, MAX_ID_LENGTH } from "../helpers";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Badge } from "components/primitives/badge";
+import { Switch } from "components/primitives/switch";
+import { Skeleton } from "components/primitives/skeleton";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { ArrowLeft, Plus, Trash2, RotateCw, Info } from "lucide-react";
+import { VpnDevice } from "@dappnode/types";
+
+export function OpenVpnDevicesHome() {
+ const navigate = useNavigate();
+ const [input, setInput] = useState("");
+ const devicesReq = useApi.devicesList();
+ const dnpRequest = useApi.packageGet({ dnpName: vpnDnpName });
+ const dnp = dnpRequest.data;
+
+ /* ── Actions ────────────────────────────────────────────────────── */
+
+ async function addDevice(id: string) {
+ if (!id) return;
+ const toastId = toast.loading(`Adding ${id}...`);
+ try {
+ await api.deviceAdd({ id });
+ toast.success(`Added ${id}`, { id: toastId });
+ setInput("");
+ devicesReq.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ async function removeDevice(id: string) {
+ const toastId = toast.loading(`Removing ${id}...`);
+ try {
+ await api.deviceRemove({ id });
+ toast.success(`Removed ${id}`, { id: toastId });
+ devicesReq.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ async function resetDevice(id: string) {
+ const toastId = toast.loading(`Resetting ${id}...`);
+ try {
+ await api.deviceReset({ id });
+ toast.success(`Reset ${id}`, { id: toastId });
+ devicesReq.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ async function toggleAdmin(id: string, isAdmin: boolean) {
+ const toastId = toast.loading(`${isAdmin ? "Granting" : "Revoking"} admin for ${id}...`);
+ try {
+ await api.deviceAdminToggle({ id, isAdmin });
+ toast.success(`${isAdmin ? "Granted" : "Revoked"} admin for ${id}`, { id: toastId });
+ devicesReq.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ /* ── Validation ─────────────────────────────────────────────────── */
+
+ const inputError = input.length > MAX_ID_LENGTH ? `Name must be shorter than ${MAX_ID_LENGTH} characters` : "";
+
+ /* ── Not installed ──────────────────────────────────────────────── */
+
+ if (dnpRequest.isValidating && !dnp) {
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+ OpenVPN
+
+
+
+
+
+
+ );
+ }
+
+ if (!dnp) {
+ const notFound = dnpRequest.error?.message?.includes("No DNP was found");
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+ OpenVPN
+
+
+ {notFound ? (
+
+
+
+ {prettyDnpName(vpnDnpName)} is not installed. Install it from the Packages section.
+
+
+ ) : (
+
+ {dnpRequest.error?.message || "Error loading OpenVPN package"}
+
+ )}
+
+
+
+ );
+ }
+
+ /* ── Devices list ───────────────────────────────────────────────── */
+
+ const devices: VpnDevice[] = devicesReq.data
+ ? [...devicesReq.data].sort((d1) => (d1.id === MAIN_ADMIN_NAME ? -1 : 0))
+ : [];
+
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+
+ OpenVPN Devices
+
+ Manage OpenVPN credentials for each device.{" "}
+
+ Setup guide
+
+
+
+
+ {/* Add device */}
+
+
setInput(coerceDeviceName(e.target.value))}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && input && !inputError) {
+ addDevice(input);
+ }
+ }}
+ className="tw:flex-1"
+ />
+
addDevice(input)} disabled={!input || !!inputError}>
+
+ Add device
+
+
+ {inputError && {inputError}
}
+
+ {/* Devices list */}
+ {devicesReq.isValidating && !devicesReq.data ? (
+
+ ) : devicesReq.error ? (
+ {devicesReq.error.message}
+ ) : devices.length === 0 ? (
+ No devices found.
+ ) : (
+
+ {devices.map((device) => (
+
+
+ {device.id}
+ {device.admin && Admin }
+
+
+
+ {/* Get credentials */}
+
navigate(device.id)}>
+ Get
+
+
+ {/* Admin toggle */}
+
toggleAdmin(device.id, !device.admin)} />
+
+ {/* Reset */}
+
+
+
+
+
+
+
+
+
+ {device.id === MAIN_ADMIN_NAME
+ ? "WARNING! Resetting main admin"
+ : `Reset ${device.id} device`}
+
+
+ {device.id === MAIN_ADMIN_NAME
+ ? "Only reset the main admin credentials if you suspect unauthorized access. Download and install the new credentials IMMEDIATELY or you will lose access."
+ : "All profiles and links pointing to this device will no longer be valid."}
+
+
+
+ Cancel
+ resetDevice(device.id)}>Reset
+
+
+
+
+ {/* Remove */}
+ {device.admin ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+ Remove {device.id}
+
+ The user using this device will lose access to this DAppNode.
+
+
+
+ Cancel
+ removeDevice(device.id)}>Remove
+
+
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/wireguard/WireguardDeviceDetails.tsx b/packages/admin-ui/src/pages-new/home/settings/vpn/wireguard/WireguardDeviceDetails.tsx
new file mode 100644
index 0000000000..383d689fb3
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/wireguard/WireguardDeviceDetails.tsx
@@ -0,0 +1,156 @@
+import React, { useState, useCallback } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { useApi, apiRoutes } from "api";
+import ClipboardJS from "clipboard";
+import QrCode from "components/QrCode";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Label } from "components/primitives/label";
+import { Skeleton } from "components/primitives/skeleton";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import { ArrowLeft, Copy, Download, QrCode as QrCodeIcon, TriangleAlert } from "lucide-react";
+import { WireguardDeviceCredentials } from "@dappnode/types";
+
+export function WireguardDeviceDetails() {
+ const navigate = useNavigate();
+ const params = useParams();
+ const id = params.id || "";
+ const device = useApi.wireguardDeviceGet(id);
+
+ // Loading
+ if (device.isValidating && !device.data) {
+ return (
+
+
navigate("..")}>
+
+ Back to devices
+
+
+
+ {id}
+
+
+
+
+
+
+ );
+ }
+
+ // Error
+ if (device.error) {
+ return (
+
+
navigate("..")}>
+
+ Back to devices
+
+
+
+ {id}
+
+
+ {device.error.message}
+
+
+
+ );
+ }
+
+ if (!device.data) return null;
+
+ return (
+
+
navigate("..")}>
+
+ Back to devices
+
+
+
+ );
+}
+
+/* ── Loaded state ───────────────────────────────────────────────────── */
+
+function WireguardDeviceDetailsLoaded({ id, device }: { id: string; device: WireguardDeviceCredentials }) {
+ const [showQr, setShowQr] = useState(false);
+ const [showLocal, setShowLocal] = useState(false);
+
+ const config = showLocal ? device.configLocal : device.configRemote;
+ const configLabel = showLocal ? "local" : "remote";
+
+ // Clipboard setup
+ const clipboardRef = useCallback((node: HTMLDivElement | null) => {
+ if (node) {
+ new ClipboardJS(".wireguard-copy-btn", { container: node });
+ }
+ }, []);
+
+ return (
+
+
+
+ {id || "Device not found"}
+
+ Add the following VPN configuration in your Wireguard client. If you experience issues connecting from the
+ same network as your Dappnode, use the local credentials.
+
+
+
+ {/* Remote / Local toggle */}
+
+ setShowLocal(false)}>
+ Remote
+
+ setShowLocal(true)}>
+ Local
+
+
+
+ {/* Action buttons */}
+
+
+ {/* Config display */}
+
+
VPN {configLabel} credentials
+
+
+
+ {/* QR code */}
+ {showQr && config && (
+
+
+
+ )}
+
+ {/* Security warning */}
+
+
+
+ Beware of shoulder surfing attacks (unsolicited observers). This data grants access to your DAppNode.
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/vpn/wireguard/WireguardDevicesHome.tsx b/packages/admin-ui/src/pages-new/home/settings/vpn/wireguard/WireguardDevicesHome.tsx
new file mode 100644
index 0000000000..10aeef09bd
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/vpn/wireguard/WireguardDevicesHome.tsx
@@ -0,0 +1,210 @@
+import React, { useState } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { useNavigate } from "react-router-dom";
+import { wireguardDnpName, MAIN_ADMIN_NAME, docsUrl } from "params";
+import { prettyDnpName } from "utils/format";
+import { coerceDeviceName, MAX_ID_LENGTH } from "../helpers";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Skeleton } from "components/primitives/skeleton";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { ArrowLeft, Plus, Trash2, Info } from "lucide-react";
+
+export function WireguardDevicesHome() {
+ const navigate = useNavigate();
+ const [input, setInput] = useState("");
+ const devicesReq = useApi.wireguardDevicesGet();
+ const dnpRequest = useApi.packageGet({ dnpName: wireguardDnpName });
+ const dnp = dnpRequest.data;
+
+ /* ── Actions ────────────────────────────────────────────────────── */
+
+ async function addDevice(id: string) {
+ if (!id) return;
+ const toastId = toast.loading(`Adding ${id}...`);
+ try {
+ await api.wireguardDeviceAdd(id);
+ toast.success(`Added ${id}`, { id: toastId });
+ setInput("");
+ devicesReq.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ async function removeDevice(id: string) {
+ const toastId = toast.loading(`Removing ${id}...`);
+ try {
+ await api.wireguardDeviceRemove(id);
+ toast.success(`Removed ${id}`, { id: toastId });
+ devicesReq.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ /* ── Validation ─────────────────────────────────────────────────── */
+
+ const inputError = input.length > MAX_ID_LENGTH ? `Name must be shorter than ${MAX_ID_LENGTH} characters` : "";
+
+ /* ── Not installed ──────────────────────────────────────────────── */
+
+ if (dnpRequest.isValidating && !dnp) {
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+ Wireguard
+
+
+
+
+
+
+ );
+ }
+
+ if (!dnp) {
+ const notFound = dnpRequest.error?.message?.includes("No DNP was found");
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+ Wireguard
+
+
+ {notFound ? (
+
+
+
+ {prettyDnpName(wireguardDnpName)} is not installed. Install it from the Packages section.
+
+
+ ) : (
+
+ {dnpRequest.error?.message || "Error loading Wireguard package"}
+
+ )}
+
+
+
+ );
+ }
+
+ /* ── Devices list ───────────────────────────────────────────────── */
+
+ const devices: string[] = devicesReq.data ? [...devicesReq.data].sort((d1) => (d1 === MAIN_ADMIN_NAME ? -1 : 0)) : [];
+
+ return (
+
+
navigate("..")}>
+
+ Back to VPN
+
+
+
+
+ Wireguard Devices
+
+ Manage Wireguard VPN devices and their credentials.{" "}
+
+ Setup guide
+
+
+
+
+ {/* Add device */}
+
+
setInput(coerceDeviceName(e.target.value))}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && input && !inputError) {
+ addDevice(input);
+ }
+ }}
+ className="tw:flex-1"
+ />
+
addDevice(input)} disabled={!input || !!inputError}>
+
+ Add device
+
+
+ {inputError && {inputError}
}
+
+ {/* Devices list */}
+ {devicesReq.isValidating && !devicesReq.data ? (
+
+ ) : devicesReq.error ? (
+ {devicesReq.error.message}
+ ) : devices.length === 0 ? (
+ No devices found.
+ ) : (
+
+ {devices.map((id) => (
+
+
{id}
+
+
+ {/* Get credentials */}
+
navigate(id)}>
+ Get
+
+
+ {/* Remove */}
+
+
+
+
+
+
+
+
+ Remove {id}
+
+ The user using this device will lose access to this DAppNode.
+
+
+
+ Cancel
+ removeDevice(id)}>Remove
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/wifi/LocalNetworkProxySection.tsx b/packages/admin-ui/src/pages-new/home/settings/wifi/LocalNetworkProxySection.tsx
new file mode 100644
index 0000000000..06dacaba86
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/wifi/LocalNetworkProxySection.tsx
@@ -0,0 +1,198 @@
+import React, { useState } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { useNavigate } from "react-router-dom";
+import { useSelector } from "react-redux";
+import { adminUiLocalDomain, docsUrl, httpsPortalDnpName } from "params";
+import { getInstallerPath } from "pages-new/utils/routeData";
+import { getDappnodeIdentityClean } from "services/dappnodeStatus/selectors";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Switch } from "components/primitives/switch";
+import { Badge } from "components/primitives/badge";
+import { Skeleton } from "components/primitives/skeleton";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { Globe, Info, TriangleAlert } from "lucide-react";
+
+export function LocalNetworkProxySection() {
+ const navigate = useNavigate();
+ const localProxyingStatus = useApi.localProxyingStatusGet();
+ const dappnodeIdentity = useSelector(getDappnodeIdentityClean);
+ const [toggling, setToggling] = useState(false);
+
+ // Loading state
+ if (localProxyingStatus.isValidating && !localProxyingStatus.data) {
+ return (
+
+
+ Local Network Proxy
+
+
+
+
+
+ );
+ }
+
+ // Error state
+ if (localProxyingStatus.error && !localProxyingStatus.data) {
+ return (
+
+
+ Local Network Proxy
+
+
+
+ {localProxyingStatus.error.message || "Error loading Local Network Proxy status"}
+
+
+
+ );
+ }
+
+ // HTTPS Portal not installed
+ if (localProxyingStatus.data === "https missing") {
+ return (
+
+
+ Local Network Proxy
+ Access your Dappnode UI from devices on the same local network.
+
+
+
+
+
+ You must{" "}
+ navigate(`${getInstallerPath(httpsPortalDnpName)}/${httpsPortalDnpName}`)}
+ >
+ install the HTTPS Portal
+ {" "}
+ to use this feature.{" "}
+
+ Learn more
+
+
+
+
+
+ );
+ }
+
+ if (!localProxyingStatus.data) return null;
+
+ const isRunning = localProxyingStatus.data === "running";
+ const isCrashed = localProxyingStatus.data === "crashed";
+
+ async function toggleProxy() {
+ const toastId = toast.loading(isRunning ? "Stopping Local Network Proxy..." : "Starting Local Network Proxy...");
+ try {
+ setToggling(true);
+ await api.localProxyingEnableDisable(!isRunning);
+ toast.success(isRunning ? "Stopped Local Network Proxy" : "Started Local Network Proxy", { id: toastId });
+ localProxyingStatus.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ } finally {
+ setToggling(false);
+ }
+ }
+
+ return (
+
+
+ Local Network Proxy
+
+ Access your Dappnode UI from devices on the same local network.{" "}
+
+ Learn more
+
+
+
+
+ {/* Reliability warning */}
+
+
+
+ Connecting via local proxy is less reliable than using a VPN or Wi-Fi hotspot and should be used{" "}
+ only as a fallback . It only provides access to the Dappmanager UI, not other package
+ interfaces.
+
+
+
+ {/* Local domain link */}
+
+ If you are on the same network as your Dappnode, access the UI at{" "}
+
+ {adminUiLocalDomain}
+
+ .
+
+
+ {/* Same-IP notice */}
+ {dappnodeIdentity.internalIp === dappnodeIdentity.ip && (
+
+ Local and public IPs are equal. Your Dappnode may be running on a remote machine and does not require Local
+ Network Proxy.
+
+ )}
+
+ {/* Status + toggle */}
+
+
+
+ Local Network Proxy
+
+ {isRunning ? "Running" : isCrashed ? "Crashed" : "Stopped"}
+
+
+ {isRunning ? (
+
+
+
+
+
+
+ Stop Local Network Proxy
+
+ If you are connected through the Local Network Proxy you may lose access to your Dappnode. Make sure
+ you have an alternative connection method (Wi-Fi or VPN).
+
+
+
+ Cancel
+ Stop
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/wifi/WifiCredentialsSection.tsx b/packages/admin-ui/src/pages-new/home/settings/wifi/WifiCredentialsSection.tsx
new file mode 100644
index 0000000000..67a65711ea
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/wifi/WifiCredentialsSection.tsx
@@ -0,0 +1,117 @@
+import React, { useState, useEffect } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { wifiDnpName, wifiEnvSSID, wifiEnvWPA_PASSPHRASE } from "params";
+import {
+ validateMinLength,
+ validateDockerEnv,
+ validateStrongPasswordAsDockerEnv,
+ validatePasswordsMatch
+} from "utils/validation";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Input } from "components/primitives/input";
+import { Label } from "components/primitives/label";
+import { Skeleton } from "components/primitives/skeleton";
+
+export function WifiCredentialsSection() {
+ const wifiCredentials = useApi.wifiCredentialsGet();
+ const [ssid, setSsid] = useState("");
+ const [password, setPassword] = useState("");
+ const [password2, setPassword2] = useState("");
+
+ useEffect(() => {
+ if (wifiCredentials.data?.ssid) setSsid(wifiCredentials.data.ssid);
+ if (wifiCredentials.data?.password) setPassword(wifiCredentials.data.password);
+ }, [wifiCredentials.data]);
+
+ const ssidError = ssid ? validateDockerEnv(ssid, "SSID") || validateMinLength(ssid, "SSID") : "";
+ const passwordError = password ? validateStrongPasswordAsDockerEnv(password) : "";
+ const password2Error = password2 ? validatePasswordsMatch(password, password2) : "";
+ const isValid = ssid && password && password2 && !ssidError && !passwordError && !password2Error;
+
+ async function onChangeCredentials() {
+ if (!isValid) return;
+ const envs = {
+ [wifiEnvSSID]: ssid,
+ [wifiEnvWPA_PASSPHRASE]: password
+ };
+ const toastId = toast.loading("Changing Wi-Fi credentials...");
+ try {
+ await api.packageSetEnvironment({
+ dnpName: wifiDnpName,
+ environmentByService: { [wifiDnpName]: envs }
+ });
+ toast.success("Wi-Fi credentials changed", { id: toastId });
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ if (wifiCredentials.isValidating && !wifiCredentials.data) {
+ return (
+
+
+ Wi-Fi Credentials
+
+
+
+
+
+ );
+ }
+
+ if (wifiCredentials.error) {
+ return (
+
+
+ Wi-Fi Credentials
+
+
+ {wifiCredentials.error.message}
+
+
+ );
+ }
+
+ return (
+
+
+ Wi-Fi Credentials
+ Change the SSID and password for your Dappnode Wi-Fi hotspot.
+
+
+
+
SSID
+
setSsid(e.target.value)} aria-invalid={!!ssidError} />
+ {ssidError &&
{ssidError}
}
+
+
+
New Password
+
setPassword(e.target.value)}
+ aria-invalid={!!passwordError}
+ />
+ {passwordError &&
{passwordError}
}
+
+
+
Confirm Password
+
setPassword2(e.target.value)}
+ aria-invalid={!!password2Error}
+ />
+ {password2Error &&
{password2Error}
}
+
+
+ Change Credentials
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/wifi/WifiStatusSection.tsx b/packages/admin-ui/src/pages-new/home/settings/wifi/WifiStatusSection.tsx
new file mode 100644
index 0000000000..45f7250eb4
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/wifi/WifiStatusSection.tsx
@@ -0,0 +1,131 @@
+import React, { useEffect } from "react";
+import { api, useApi } from "api";
+import { toast } from "sonner";
+import { wifiDnpName } from "params";
+import { prettyDnpName } from "utils/format";
+import { continueIfCalleDisconnected } from "api/utils";
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "components/primitives/card";
+import { Switch } from "components/primitives/switch";
+import { Badge } from "components/primitives/badge";
+import { Skeleton } from "components/primitives/skeleton";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger
+} from "components/primitives/alert-dialog";
+import { Wifi, WifiOff } from "lucide-react";
+
+export function WifiStatusSection() {
+ const wifiDnp = useApi.packageGet({ dnpName: wifiDnpName });
+ const wifiReport = useApi.wifiReportGet();
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ wifiReport.revalidate();
+ }, 5000);
+ return () => clearInterval(interval);
+ }, [wifiReport]);
+
+ if (wifiDnp.isValidating && !wifiDnp.data) {
+ return (
+
+
+ Wi-Fi Status
+
+
+
+
+
+ );
+ }
+
+ if (!wifiDnp.data) {
+ const notInstalled = wifiDnp.error?.message?.includes("No DNP was found");
+ return (
+
+
+ Wi-Fi
+
+
+
+ {notInstalled
+ ? `The Wi-Fi package (${prettyDnpName(wifiDnpName)}) is not installed.`
+ : `Error loading Wi-Fi package: ${wifiDnp.error?.message || "Unknown error"}`}
+
+
+
+ );
+ }
+
+ const container = wifiDnp.data.containers[0];
+ const isRunning = container?.state === "running";
+
+ async function toggleWifi() {
+ const toastId = toast.loading(isRunning ? "Pausing Wi-Fi..." : "Starting Wi-Fi...");
+ try {
+ await continueIfCalleDisconnected(() => api.packageStartStop({ dnpName: wifiDnpName }), wifiDnpName)();
+ toast.success(isRunning ? "Wi-Fi paused" : "Wi-Fi started", { id: toastId });
+ wifiDnp.revalidate();
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+ }
+
+ return (
+
+
+ Wi-Fi Status
+ Manage the Wi-Fi hotspot exposed by your Dappnode.
+
+
+
+
+ {isRunning ? (
+
+ ) : (
+
+ )}
+ Wi-Fi Service
+ {isRunning ? "Running" : "Stopped"}
+
+ {isRunning ? (
+
+
+
+
+
+
+ Pause Wi-Fi service
+
+ If you are connected through Wi-Fi, you may lose access to your Dappnode. Make sure you have an
+ alternative connection method.
+
+
+
+ Cancel
+ Pause
+
+
+
+ ) : (
+
+ )}
+
+
+ {wifiReport.data?.info && {wifiReport.data.info}
}
+ {wifiReport.data?.report && (
+
+ {wifiReport.data.report.lastLog}
+ {wifiReport.data.report.exitCode !== undefined && `. Exit code: ${wifiReport.data.report.exitCode}`}
+
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/wifi/WifiTab.tsx b/packages/admin-ui/src/pages-new/home/settings/wifi/WifiTab.tsx
new file mode 100644
index 0000000000..956d8382c9
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/wifi/WifiTab.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Separator } from "components/primitives/separator";
+import { WifiStatusSection } from "./WifiStatusSection";
+import { WifiCredentialsSection } from "./WifiCredentialsSection";
+import { LocalNetworkProxySection } from "./LocalNetworkProxySection";
+
+export function WifiTab() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/home/settings/wifi/index.ts b/packages/admin-ui/src/pages-new/home/settings/wifi/index.ts
new file mode 100644
index 0000000000..73d38f84a8
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/home/settings/wifi/index.ts
@@ -0,0 +1 @@
+export { WifiTab } from "./WifiTab";
diff --git a/packages/admin-ui/src/pages-new/layouts/NewPageLayout.tsx b/packages/admin-ui/src/pages-new/layouts/NewPageLayout.tsx
new file mode 100644
index 0000000000..1666628d02
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/layouts/NewPageLayout.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { DecorativeBackground } from "layouts/DecorativeBackground";
+
+/**
+ * Base layout wrapper for all new pages in `pages-new/`.
+ *
+ * Provides:
+ * - `.tw-base` scoped reset (box-sizing, font, etc.)
+ * - `tw:bg-background` + `tw:min-h-screen`
+ * - Optional decorative gradient-orb background
+ * - `tw:overflow-hidden` to clip orbs that bleed outside the viewport
+ *
+ * Usage:
+ * ```tsx
+ *
+ *
+ *
+ *
+ * // Without decorative background:
+ *
+ *
+ *
+ * ```
+ */
+export function NewPageLayout({
+ children,
+ decorativeBackground = true,
+ className
+}: {
+ children: React.ReactNode;
+ /** Show the gradient-orb decorative background. Defaults to `true`. */
+ decorativeBackground?: boolean;
+ /** Extra classes merged onto the outer wrapper. */
+ className?: string;
+}) {
+ return (
+
+ {decorativeBackground &&
}
+
+ {/* Content sits above the decorative layer */}
+
{children}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/layouts/index.ts b/packages/admin-ui/src/pages-new/layouts/index.ts
new file mode 100644
index 0000000000..42dc40110d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/layouts/index.ts
@@ -0,0 +1 @@
+export { NewPageLayout } from "./NewPageLayout";
diff --git a/packages/admin-ui/src/pages-new/packages/PackageDetailPage.tsx b/packages/admin-ui/src/pages-new/packages/PackageDetailPage.tsx
new file mode 100644
index 0000000000..0019e4b364
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/PackageDetailPage.tsx
@@ -0,0 +1,197 @@
+import React from "react";
+import { useParams, NavLink, Routes, Route, Navigate, useNavigate } from "react-router-dom";
+import { useApi } from "api";
+import { isEmpty } from "lodash-es";
+import { prettyDnpName } from "utils/format";
+import { InstalledPackageDetailData } from "@dappnode/types";
+import { Skeleton } from "components/primitives/skeleton";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import { Button } from "components/primitives/button";
+import { PageContainer, PageTitle } from "components/primitives/page";
+import {
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuLink
+} from "components/primitives/navigation-menu";
+import { ArrowLeft, TriangleAlert, ArrowUpCircle } from "lucide-react";
+import defaultAvatar from "img/defaultAvatar.png";
+
+import { InfoTab } from "pages-new/ai/packages/tabs/info";
+import { ConfigTab } from "pages-new/ai/packages/tabs/ConfigTab";
+import { LogsTab } from "pages-new/ai/packages/tabs/LogsTab";
+import { NetworkTab } from "pages-new/ai/packages/tabs/network";
+import { FileManagerTab } from "pages-new/ai/packages/tabs/FileManagerTab";
+import { BackupTab } from "pages-new/ai/packages/tabs/BackupTab";
+import { PackagesConfig } from "./config";
+
+/* ── Tab definitions ────────────────────────────────────────────────── */
+
+interface TabDef {
+ label: string;
+ subPath: string;
+ render: (dnp: InstalledPackageDetailData) => React.ReactNode;
+ available: (dnp: InstalledPackageDetailData) => boolean;
+}
+
+const tabDefs: TabDef[] = [
+ {
+ label: "Info",
+ subPath: "info",
+ render: (dnp) => ,
+ available: () => true
+ },
+ {
+ label: "Config",
+ subPath: "config",
+ render: (dnp) => ,
+ available: (dnp) => Boolean(dnp.userSettings && !isEmpty(dnp.userSettings.environment))
+ },
+ {
+ label: "Network",
+ subPath: "network",
+ render: (dnp) => ,
+ available: (dnp) => dnp.dnpName !== "dappmanager.dnp.dappnode.eth"
+ },
+ {
+ label: "Logs",
+ subPath: "logs",
+ render: (dnp) => ,
+ available: () => true
+ },
+ {
+ label: "Backup",
+ subPath: "backup",
+ render: (dnp) => ,
+ available: (dnp) => (dnp.backup || []).length > 0
+ },
+ {
+ label: "File Manager",
+ subPath: "file-manager",
+ render: (dnp) => ,
+ available: () => true
+ }
+];
+
+/* ── Props ──────────────────────────────────────────────────────────── */
+
+interface PackageDetailPageProps {
+ config: PackagesConfig;
+}
+
+/* ── Component ──────────────────────────────────────────────────────── */
+
+export function PackageDetailPage({ config }: PackageDetailPageProps) {
+ const params = useParams();
+ const id = params.id || "";
+ const navigate = useNavigate();
+
+ const dnpRequest = useApi.packageGet({ dnpName: id });
+ const dnp = dnpRequest.data;
+
+ /* ── Loading ──────────────────────────────────────────────────────── */
+ if (!dnp && dnpRequest.isValidating) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ /* ── Error ────────────────────────────────────────────────────────── */
+ if (!dnp) {
+ const notFound = dnpRequest.error?.message?.includes("package not found");
+ return (
+
+
+
+
+ {notFound ? "Package not found" : "Error"}
+
+ {notFound
+ ? `The package "${prettyDnpName(id)}" is not installed.`
+ : dnpRequest.error?.message || "Failed to load package."}
+
+
+
+ );
+ }
+
+ const availableTabs = tabDefs.filter((t) => t.available(dnp));
+
+ return (
+
+ {/* Back + title */}
+
+
+
+
+
+
{prettyDnpName(dnp.dnpName)}
+
v{dnp.version}
+
+
+
+
+ {/* Update available banner */}
+ {dnp.updateAvailable && (
+
+
+ Update available
+
+
+ Version {dnp.updateAvailable.newVersion}
+ {dnp.updateAvailable.upstreamVersion && ` (${dnp.updateAvailable.upstreamVersion} upstream)`}
+
+ navigate(`${config.installerPath}/${encodeURIComponent(dnp.dnpName)}`)}
+ >
+ Update
+
+
+
+ )}
+
+ {/* Tab navbar */}
+
+
+ {availableTabs.map((tab) => (
+
+
+ {tab.label}
+
+
+ ))}
+
+
+
+ {/* Tab content */}
+
+ {availableTabs.map((tab) => (
+
+ ))}
+ } />
+
+
+ );
+}
+
+/* ── Back button ────────────────────────────────────────────────────── */
+
+function BackButton({ packagesPath }: { packagesPath: string }) {
+ const navigate = useNavigate();
+ return (
+ navigate(packagesPath)} className="tw:inline-flex tw:self-start">
+
+ Packages
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/packages/PackagesPage.tsx b/packages/admin-ui/src/pages-new/packages/PackagesPage.tsx
new file mode 100644
index 0000000000..28bfed93a5
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/PackagesPage.tsx
@@ -0,0 +1,213 @@
+import React, { useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useApi } from "api";
+import { sortBy } from "lodash-es";
+import { prettyDnpName } from "utils/format";
+import { ClickableCard, CardHeader, CardTitle, CardDescription, CardContent } from "components/primitives/card";
+import { Badge } from "components/primitives/badge";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from "components/primitives/empty";
+import { Skeleton } from "components/primitives/skeleton";
+import { PageContainer, PageHeader } from "components/primitives/page";
+import { Separator } from "components/primitives/separator";
+import { TypographyH4 } from "components/primitives/typography";
+import {
+ CircleCheck,
+ CirclePause,
+ CircleX,
+ RefreshCw,
+ TriangleAlert,
+ PackageOpen,
+ ChevronDown,
+ ChevronUp
+} from "lucide-react";
+import { InstalledPackageDataApiReturn } from "@dappnode/types";
+import { parseContainerState, SimpleState } from "pages/packages/components/StateBadge/utils";
+import defaultAvatar from "img/defaultAvatar.png";
+import { Button } from "components/primitives/button";
+import { coreDnpName, tailscaleDnpName } from "params";
+import { PackagesConfig, matchesInstalledFilter } from "./config";
+
+/* ── Status helpers ─────────────────────────────────────────────────── */
+
+const statusIcon: Record = {
+ running: ,
+ stopped: ,
+ crashed: ,
+ restarting: ,
+ removing:
+};
+
+const statusClass: Record = {
+ running: "tw:text-green-600 tw:dark:text-green-400",
+ stopped: "tw:text-muted-foreground",
+ crashed: "tw:text-destructive",
+ restarting: "tw:text-amber-500",
+ removing: "tw:text-muted-foreground"
+};
+
+function getAggregateState(dnp: InstalledPackageDataApiReturn): SimpleState {
+ const states = dnp.containers.map((c) => parseContainerState(c).state);
+ if (states.includes("crashed")) return "crashed";
+ if (states.includes("restarting")) return "restarting";
+ if (states.every((s) => s === "stopped")) return "stopped";
+ return "running";
+}
+
+/* ── Props ──────────────────────────────────────────────────────────── */
+
+interface PackagesPageProps {
+ config: PackagesConfig;
+}
+
+/* ── Component ──────────────────────────────────────────────────────── */
+
+export function PackagesPage({ config }: PackagesPageProps) {
+ const navigate = useNavigate();
+ const dnpsRequest = useApi.packagesGet();
+ const dnps = dnpsRequest.data;
+ const error = dnpsRequest.error;
+ const loading = dnpsRequest.isValidating && !dnps;
+ const [showSystem, setShowSystem] = useState(false);
+
+ /** Filter non-core packages by the section's category config. */
+ const packages = useMemo(() => {
+ if (!dnps) return [];
+ return sortBy(
+ dnps.filter((d) => !d.isCore && matchesInstalledFilter(d, config.categoryFilter)),
+ (d) => d.dnpName
+ );
+ }, [dnps, config.categoryFilter]);
+
+ /** System (core) packages + Tailscale if installed. */
+ const systemPackages = useMemo(() => {
+ if (!dnps) return [];
+ return sortBy(
+ dnps.filter((d) => (d.isCore && d.dnpName !== coreDnpName) || d.dnpName === tailscaleDnpName),
+ (d) => d.dnpName
+ );
+ }, [dnps]);
+
+ const description = `View and manage the ${config.sectionLabel} packages installed on your Dappnode. Monitor the status, versions and updates.`;
+
+ if (loading) {
+ return (
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+ Failed to load packages
+ {error.message}
+
+
+ );
+ }
+
+ return (
+
+
+
+ {packages.length === 0 ? (
+
+
+
+
+
+ No packages installed
+
+ Head over to the Store to install your first {config.sectionLabel} package.
+
+ navigate(config.storePath)}>
+ Go to Store
+
+
+
+ ) : (
+
+ )}
+
+ {/* Show system packages toggle */}
+
+ setShowSystem((s) => !s)}
+ className="tw:w-full tw:justify-center tw:gap-1.5"
+ >
+ {showSystem ? "Hide" : "Show"} system packages ({systemPackages.length})
+ {showSystem ? : }
+
+
+ {showSystem && systemPackages.length > 0 && (
+
+ )}
+
+ );
+}
+
+/* ── Package grid ───────────────────────────────────────────────────── */
+
+function PackageGrid({
+ packages,
+ navigate,
+ packagesPath
+}: {
+ packages: InstalledPackageDataApiReturn[];
+ navigate: ReturnType;
+ packagesPath: string;
+}) {
+ return (
+
+ {packages.map((dnp) => {
+ const state = getAggregateState(dnp);
+ const prettyName = prettyDnpName(dnp.dnpName);
+ return (
+
navigate(`${packagesPath}/${encodeURIComponent(dnp.dnpName)}/info`)}
+ >
+
+
+
+ {prettyName}
+ v{dnp.version}
+
+
+
+
+ {statusIcon[state]}
+ {state}
+
+ {dnp.updateAvailable && (
+
+ Update available
+
+ )}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/packages/config.ts b/packages/admin-ui/src/pages-new/packages/config.ts
new file mode 100644
index 0000000000..f81f462cb3
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/config.ts
@@ -0,0 +1,46 @@
+import { InstalledPackageDataApiReturn, DirectoryItemOk } from "@dappnode/types";
+
+/* ── Filter configuration ───────────────────────────────────────────── */
+
+/**
+ * Describes how to filter packages for a given section (AI, Staking, etc.).
+ *
+ * - `include` keeps only packages whose categories include at least one of the listed values.
+ * - `exclude` keeps packages that do NOT have any of the listed categories.
+ *
+ * Exactly one of `include` or `exclude` should be set.
+ */
+export type CategoryFilter = { mode: "include"; categories: string[] } | { mode: "exclude"; categories: string[] };
+
+/** Configuration that each section passes to the shared Packages / Store pages. */
+export interface PackagesConfig {
+ /** Human-readable section label, e.g. "AI", "Staking" */
+ sectionLabel: string;
+ /** Category filter applied to directory items and installed packages */
+ categoryFilter: CategoryFilter;
+ /** Absolute path to the packages list page, e.g. "/ai/packages" */
+ packagesPath: string;
+ /** Absolute path to the store page, e.g. "/ai/store" */
+ storePath: string;
+ /** Absolute path prefix for the installer, e.g. "/installer" */
+ installerPath: string;
+}
+
+/* ── Filter helpers ─────────────────────────────────────────────────── */
+
+/** Returns true if the directory item passes the category filter. */
+export function matchesDirectoryFilter(item: DirectoryItemOk, filter: CategoryFilter): boolean {
+ if (filter.mode === "include") {
+ return filter.categories.some((cat) => item.categories.includes(cat));
+ }
+ return !filter.categories.some((cat) => item.categories.includes(cat));
+}
+
+/** Returns true if the installed package passes the category filter. */
+export function matchesInstalledFilter(pkg: InstalledPackageDataApiReturn, filter: CategoryFilter): boolean {
+ const cats = pkg.categories ?? [];
+ if (filter.mode === "include") {
+ return filter.categories.some((cat) => cats.includes(cat));
+ }
+ return !filter.categories.some((cat) => cats.includes(cat));
+}
diff --git a/packages/admin-ui/src/pages-new/packages/index.ts b/packages/admin-ui/src/pages-new/packages/index.ts
new file mode 100644
index 0000000000..e1db536050
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/index.ts
@@ -0,0 +1,5 @@
+export { PackagesPage } from "./PackagesPage";
+export { PackageDetailPage } from "./PackageDetailPage";
+export { StorePage } from "./store/StorePage";
+export type { PackagesConfig, CategoryFilter } from "./config";
+export { matchesDirectoryFilter, matchesInstalledFilter } from "./config";
diff --git a/packages/admin-ui/src/pages-new/packages/store/StoreGrid.tsx b/packages/admin-ui/src/pages-new/packages/store/StoreGrid.tsx
new file mode 100644
index 0000000000..fd60d9e594
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/store/StoreGrid.tsx
@@ -0,0 +1,19 @@
+import React from "react";
+import { DirectoryItemOk } from "@dappnode/types";
+import { StorePackageCard } from "./StorePackageCard";
+
+export function StoreGrid({
+ packages,
+ onPackageClick
+}: {
+ packages: DirectoryItemOk[];
+ onPackageClick: (item: DirectoryItemOk) => void;
+}) {
+ return (
+
+ {packages.map((item) => (
+ onPackageClick(item)} />
+ ))}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/packages/store/StoreGridSkeleton.tsx b/packages/admin-ui/src/pages-new/packages/store/StoreGridSkeleton.tsx
new file mode 100644
index 0000000000..638274fdc6
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/store/StoreGridSkeleton.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { Card, CardHeader, CardContent, CardFooter } from "components/primitives/card";
+import { Skeleton } from "components/primitives/skeleton";
+
+/**
+ * Skeleton placeholder matching the shape of `StorePackageCard`.
+ * Rendered while the directory is loading.
+ */
+export function StorePackageCardSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/**
+ * A grid of skeleton cards used as the loading state for the Store page.
+ *
+ * @param count Number of skeleton cards to render. Defaults to 6.
+ */
+export function StoreGridSkeleton({ count = 6 }: { count?: number }) {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/packages/store/StorePackageCard.tsx b/packages/admin-ui/src/pages-new/packages/store/StorePackageCard.tsx
new file mode 100644
index 0000000000..1b79034170
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/store/StorePackageCard.tsx
@@ -0,0 +1,88 @@
+import React from "react";
+import { DirectoryItemOk } from "@dappnode/types";
+import { ClickableCard, CardHeader, CardContent, CardFooter } from "components/primitives/card";
+import { Badge } from "components/primitives/badge";
+import { TypographyMuted } from "components/primitives/typography";
+import defaultAvatar from "img/defaultAvatar.png";
+import { prettyDnpName } from "utils/format";
+import { CheckCircle, ArrowUpCircle, Download } from "lucide-react";
+
+/**
+ * A package card for the AI Store grid.
+ *
+ * Displays the package avatar, name, description, install status badge,
+ * and categories. The entire card is clickable.
+ */
+export function StorePackageCard({ item, onClick }: { item: DirectoryItemOk; onClick: () => void }) {
+ return (
+
+
+
+
+
+
+ {prettyDnpName(item.name)}
+
+ {item.name}
+
+
+
+
+
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+ {item.categories.map((cat) => (
+
+ {cat}
+
+ ))}
+
+
+
+
+ );
+}
+
+function StatusIndicator({ isInstalled, isUpdated }: { isInstalled: boolean; isUpdated: boolean }) {
+ if (isUpdated) {
+ return (
+
+
+
+ );
+ }
+ if (isInstalled) {
+ return (
+
+ );
+ }
+ return (
+
+
+
+ );
+}
+
+/**
+ * Small text badge in the footer reinforcing the status.
+ */
+function StatusBadge({ isInstalled, isUpdated }: { isInstalled: boolean; isUpdated: boolean }) {
+ if (isUpdated) {
+ return Up to date ;
+ }
+ if (isInstalled) {
+ return Update available ;
+ }
+ return Install ;
+}
diff --git a/packages/admin-ui/src/pages-new/packages/store/StorePage.tsx b/packages/admin-ui/src/pages-new/packages/store/StorePage.tsx
new file mode 100644
index 0000000000..1b2f6ee868
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/store/StorePage.tsx
@@ -0,0 +1,83 @@
+import React, { useEffect, useMemo } from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { useNavigate } from "react-router-dom";
+import { DirectoryItemOk } from "@dappnode/types";
+import { getDnpDirectory, getDirectoryRequestStatus } from "services/dnpDirectory/selectors";
+import { fetchDnpDirectory } from "services/dnpDirectory/actions";
+import { PageContainer, PageHeader } from "components/primitives/page";
+import { Alert, AlertTitle, AlertDescription } from "components/primitives/alert";
+import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from "components/primitives/empty";
+import { PackageOpen, TriangleAlert } from "lucide-react";
+import { StoreGrid } from "./StoreGrid";
+import { StoreGridSkeleton } from "./StoreGridSkeleton";
+import { PackagesConfig, matchesDirectoryFilter } from "../config";
+
+interface StorePageProps {
+ config: PackagesConfig;
+}
+
+/**
+ * Shared Store page — displays a grid of DNP packages from the on-chain
+ * directory, filtered by the supplied category configuration.
+ */
+export function StorePage({ config }: StorePageProps) {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+ const directory = useSelector(getDnpDirectory);
+ const requestStatus = useSelector(getDirectoryRequestStatus);
+
+ useEffect(() => {
+ dispatch(fetchDnpDirectory());
+ }, [dispatch]);
+
+ /** Filter directory items by the section's category config. */
+ const filteredPackages = useMemo(
+ () =>
+ directory.filter(
+ (item): item is DirectoryItemOk => item.status === "ok" && matchesDirectoryFilter(item, config.categoryFilter)
+ ),
+ [directory, config.categoryFilter]
+ );
+
+ function handlePackageClick(item: DirectoryItemOk) {
+ const encodedName = encodeURIComponent(item.name);
+ if (item.isUpdated) {
+ navigate(`${config.packagesPath}/${encodedName}/info`);
+ } else {
+ navigate(`${config.installerPath}/${encodedName}`);
+ }
+ }
+
+ return (
+
+
+
+ {requestStatus.loading && !directory.length ? (
+
+ ) : requestStatus.error ? (
+
+
+ Failed to load packages
+ {requestStatus.error}
+
+ ) : filteredPackages.length === 0 ? (
+
+
+
+
+
+ No packages found
+
+ There are no matching packages in the Dappnode directory yet. Check back soon!
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/packages/store/index.ts b/packages/admin-ui/src/pages-new/packages/store/index.ts
new file mode 100644
index 0000000000..ce1f90eb08
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/packages/store/index.ts
@@ -0,0 +1,4 @@
+export { StorePackageCard } from "./StorePackageCard";
+export { StoreGrid } from "./StoreGrid";
+export { StoreGridSkeleton, StorePackageCardSkeleton } from "./StoreGridSkeleton";
+export { StorePage } from "./StorePage";
diff --git a/packages/admin-ui/src/pages-new/staking/StakingLayout.tsx b/packages/admin-ui/src/pages-new/staking/StakingLayout.tsx
new file mode 100644
index 0000000000..1ddf99619b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/StakingLayout.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import { Routes, Route, Navigate } from "react-router-dom";
+import { LayoutDashboard, Settings2, Package, ShoppingBag } from "lucide-react";
+import { SectionLayout, NavItem } from "layouts";
+import { DashboardPage } from "./dashboard";
+import { StakersPage } from "./stakers";
+import { PackagesPage, PackageDetailPage, StorePage } from "pages-new/packages";
+import { BannerNotifications } from "../home/BannerNotifications";
+import { stakingPackagesConfig } from "./packagesConfig";
+
+/* ── Navigation items ───────────────────────────────────────────────── */
+
+const navItems: NavItem[] = [
+ { label: "Dashboard", icon: LayoutDashboard, path: "/staking/dashboard" },
+ { label: "Stakers", icon: Settings2, path: "/staking/stakers" },
+ { label: "Packages", icon: Package, path: stakingPackagesConfig.packagesPath },
+ { label: "Store", icon: ShoppingBag, path: stakingPackagesConfig.storePath }
+];
+
+/* ── Layout ─────────────────────────────────────────────────────────── */
+
+export function StakingLayout() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/dashboard/ClientRow.tsx b/packages/admin-ui/src/pages-new/staking/dashboard/ClientRow.tsx
new file mode 100644
index 0000000000..58f9d1712b
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/dashboard/ClientRow.tsx
@@ -0,0 +1,130 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import { Badge } from "components/primitives/badge";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "components/primitives/tooltip";
+import { Info } from "lucide-react";
+import { capitalize } from "utils/strings";
+import { withLegacyBase } from "utils/path";
+
+export function ClientRow({
+ label,
+ network: _network,
+ dnpName,
+ clientData,
+ clientError,
+ isInstalled,
+ showWaiting = false,
+ showProgress = false,
+ progress
+}: {
+ label: string;
+ network: string;
+ dnpName: string | null;
+ clientData: { name: string; isSynced: boolean; currentBlock: number; peers: number; progress: number } | null;
+ clientError: { error: string } | null;
+ isInstalled: boolean;
+ showWaiting?: boolean;
+ showProgress?: boolean;
+ progress?: number;
+}) {
+ const parseClientName = (name: string) => capitalize(name.split(".")[0].split("-")[0] ?? "-");
+
+ const clientLink = dnpName ? (
+
+ {clientData ? capitalize(clientData.name ?? "-") : parseClientName(dnpName)}
+
+ ) : clientData ? (
+ capitalize(clientData.name ?? "-")
+ ) : (
+ "-"
+ );
+
+ if (!isInstalled) {
+ return (
+
+
{label}
+
Not installed
+
+ );
+ }
+
+ if (clientError) {
+ return (
+
+
+
{label}
+
{clientLink}
+
+
+
+
+
+
+ Error
+
+
+ {clientError.error}
+
+
+
+ );
+ }
+
+ if (clientData) {
+ return (
+
+
+
+
{label}
+
{clientLink}
+
+
+
Peers: {clientData.peers}
+ {showWaiting ? (
+
+
+
+
+
+ Waiting
+
+
+
+ {label} client status will be available once the consensus client finishes syncing.
+
+
+
+ ) : (
+ <>
+
#{clientData.currentBlock}
+
+ {clientData.isSynced ? "Synced" : "Syncing"}
+
+ >
+ )}
+
+
+ {showProgress && progress !== undefined && (
+
+
+
+
+
+ Syncing progress: {progress}%
+
+
+ )}
+
+ );
+ }
+
+ return null;
+}
diff --git a/packages/admin-ui/src/pages-new/staking/dashboard/DashboardPage.tsx b/packages/admin-ui/src/pages-new/staking/dashboard/DashboardPage.tsx
new file mode 100644
index 0000000000..57760139ef
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/dashboard/DashboardPage.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import { PageContainer, PageHeader } from "components/primitives/page";
+import { SystemHealthSection } from "./SystemHealthSection";
+import { NetworkStatsSection } from "./NetworkStatsSection";
+import { Separator } from "components/primitives/separator";
+
+export function DashboardPage() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/dashboard/NetworkStatsSection.tsx b/packages/admin-ui/src/pages-new/staking/dashboard/NetworkStatsSection.tsx
new file mode 100644
index 0000000000..6f07438cd8
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/dashboard/NetworkStatsSection.tsx
@@ -0,0 +1,92 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import { useNetworkStats } from "hooks/useNetworkStats";
+import { DashboardSupportedNetwork, Network } from "@dappnode/types";
+import { Skeleton } from "components/primitives/skeleton";
+import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from "components/primitives/empty";
+import { TypographyH4 } from "components/primitives/typography";
+import { ServerOff } from "lucide-react";
+import { capitalize } from "utils/strings";
+import { NodeStatusCard } from "./NodeStatusCard";
+import { ValidatorsCard } from "./ValidatorsCard";
+import { RewardsCard } from "./RewardsCard";
+
+export function NetworkStatsSection() {
+ const {
+ networkStats,
+ clientsLoading,
+ getNetworkLogo,
+ networksWithClients,
+ isNetworkNodeLoading,
+ isNetworkValidatorsLoading
+ } = useNetworkStats();
+
+ if (clientsLoading) {
+ return (
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ if (!networksWithClients || networksWithClients.length === 0) {
+ return (
+
+
+
+
+
+ No nodes configured yet
+
+ You haven't set up a node on any network.{" "}
+
+ Set up your nodes from the Stakers tab.
+
+
+
+
+ );
+ }
+
+ return (
+
+ {networksWithClients.map((network) => {
+ const data = networkStats[network];
+ const NetworkLogo = getNetworkLogo(network as DashboardSupportedNetwork);
+ const nodeLoading = isNetworkNodeLoading(network);
+ const validatorsLoading = isNetworkValidatorsLoading(network);
+
+ return (
+
+
+
+ {network === Network.Mainnet ? "Ethereum" : capitalize(network)}
+
+
+
+
+
+ {data && data.hasValidators && (
+
+ )}
+
+ {data && data.beaconExplorer && data.validators && data.validators.total > 0 && (
+
+ )}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/dashboard/NodeStatusCard.tsx b/packages/admin-ui/src/pages-new/staking/dashboard/NodeStatusCard.tsx
new file mode 100644
index 0000000000..edb0ad642d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/dashboard/NodeStatusCard.tsx
@@ -0,0 +1,87 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { isClientError } from "hooks/useNetworkStats";
+import { Network, NodeStatus } from "@dappnode/types";
+import { Card, CardHeader, CardTitle, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Skeleton } from "components/primitives/skeleton";
+import { Separator } from "components/primitives/separator";
+import { Activity } from "lucide-react";
+import { ClientRow } from "./ClientRow";
+
+export function NodeStatusCard({
+ network,
+ data,
+ clientsLoading,
+ clientsDnps
+}: {
+ network: string;
+ data: NodeStatus | undefined;
+ clientsLoading: boolean;
+ clientsDnps?: { ecDnp: string | null; ccDnp: string | null };
+}) {
+ const navigate = useNavigate();
+
+ const ecResult = data?.ec ?? null;
+ const ccResult = data?.cc ?? null;
+ const ecError = ecResult && isClientError(ecResult) ? ecResult : null;
+ const ccError = ccResult && isClientError(ccResult) ? ccResult : null;
+ const execution = ecResult && !isClientError(ecResult) ? ecResult : null;
+ const consensus = ccResult && !isClientError(ccResult) ? ccResult : null;
+ const consensusSynced = consensus?.isSynced ?? false;
+
+ return (
+
+
+
+
+
+ {clientsLoading ? (
+
+
+
+
+ ) : data ? (
+ <>
+
+
+
+ >
+ ) : (
+ Data could not be fetched
+ )}
+
+
+ navigate(`/staking/stakers/${network === Network.Mainnet ? "ethereum" : network}`)}
+ >
+ View Setup
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/dashboard/RewardsCard.tsx b/packages/admin-ui/src/pages-new/staking/dashboard/RewardsCard.tsx
new file mode 100644
index 0000000000..3b44adea8d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/dashboard/RewardsCard.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import { Network } from "@dappnode/types";
+import { Card, CardHeader, CardTitle, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Trophy, ExternalLink } from "lucide-react";
+import newTabProps from "utils/newTabProps";
+
+export function RewardsCard({
+ network,
+ beaconExplorer,
+ pubKeys
+}: {
+ network: string;
+ beaconExplorer: { url: string; name: string };
+ pubKeys?: string[];
+}) {
+ const getDashboardUrl = () => {
+ const baseUrl = beaconExplorer.url;
+ if (pubKeys && pubKeys.length > 0 && (network === Network.Mainnet || network === Network.Hoodi)) {
+ return `${baseUrl}dashboard?validators=${pubKeys.join(",")}`;
+ }
+ return baseUrl;
+ };
+
+ return (
+
+
+
+
+ Rewards
+
+
+
+
+ View your detailed validator rewards in the explorer.
+
+
+
+
+
+ Visit {beaconExplorer.name}
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/dashboard/SystemHealthSection.tsx b/packages/admin-ui/src/pages-new/staking/dashboard/SystemHealthSection.tsx
new file mode 100644
index 0000000000..da5fe8da84
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/dashboard/SystemHealthSection.tsx
@@ -0,0 +1,144 @@
+import React from "react";
+import { useSystemHealth } from "hooks/useSystemHealth";
+import { Card, CardHeader, CardTitle, CardContent } from "components/primitives/card";
+import { Skeleton } from "components/primitives/skeleton";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "components/primitives/tooltip";
+import { Cpu, Thermometer, MemoryStick, HardDrive, Clock } from "lucide-react";
+
+function getStatusColor(value: number, warning = 75, danger = 90): "primary" | "caution" | "destructive" {
+ if (value > danger) return "destructive";
+ if (value > warning) return "caution";
+ return "primary";
+}
+
+export function SystemHealthSection() {
+ const {
+ cpuUsage,
+ cpuTemp,
+ memoryUsed,
+ memoryTotal,
+ memoryPercentage,
+ diskUsed,
+ diskTotal,
+ diskPercentage,
+ uptime,
+ isLoading
+ } = useSystemHealth();
+
+ if (isLoading) {
+ return (
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ System Health
+
+
+
+ {/* Uptime banner */}
+ {uptime && (
+ <>
+
+
+
+ Your Dappnode has been running for:{" "}
+ {uptime}
+
+
+ >
+ )}
+
+
+ {/* Stats grid */}
+
+ } title="CPU Usage" value={`${cpuUsage}%`} percent={cpuUsage} />
+ }
+ title="CPU Temp"
+ value={`${cpuTemp}°C`}
+ percent={cpuTemp}
+ max={115}
+ warning={85}
+ danger={95}
+ />
+ }
+ title="Memory"
+ value={`${memoryUsed} / ${memoryTotal}`}
+ percent={memoryPercentage}
+ />
+ }
+ title="Disk"
+ value={`${diskUsed} / ${diskTotal}`}
+ percent={diskPercentage}
+ />
+
+
+ );
+}
+
+function StatBlock({
+ icon,
+ title,
+ value,
+ percent,
+ max = 100,
+ warning = 75,
+ danger = 90
+}: {
+ icon: React.ReactNode;
+ title: string;
+ value: string;
+ percent: number;
+ max?: number;
+ warning?: number;
+ danger?: number;
+}) {
+ const normalized = Math.min(Math.round((percent / max) * 100), 100);
+ const color = getStatusColor(percent, warning, danger);
+
+ return (
+
+
+
+ {icon}
+ {title}
+
+
{value}
+
+
+
+
+
+
+ {normalized}%
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/dashboard/ValidatorsCard.tsx b/packages/admin-ui/src/pages-new/staking/dashboard/ValidatorsCard.tsx
new file mode 100644
index 0000000000..1b146585eb
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/dashboard/ValidatorsCard.tsx
@@ -0,0 +1,129 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { Network, NetworkStatus } from "@dappnode/types";
+import { Card, CardHeader, CardTitle, CardContent } from "components/primitives/card";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { Skeleton } from "components/primitives/skeleton";
+import { Separator } from "components/primitives/separator";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "components/primitives/tooltip";
+import { Zap, AlertTriangle, ExternalLink } from "lucide-react";
+import { capitalize } from "utils/strings";
+import { gweiToToken } from "utils/gweiToToken";
+import newTabProps from "utils/newTabProps";
+import { withLegacyBase } from "utils/path";
+
+export function ValidatorsCard({
+ network,
+ validatorsLoading,
+ data
+}: {
+ network: string;
+ validatorsLoading: boolean;
+ data: NetworkStatus["validators"];
+}) {
+ const navigate = useNavigate();
+ const signerInstalled = data?.signerStatus.isInstalled;
+ const brainRunning = data?.signerStatus.brainRunning;
+
+ const brainUrl =
+ network === Network.Mainnet ? "http://brain.web3signer.dappnode" : `http://brain.web3signer-${network}.dappnode`;
+
+ return (
+
+
+
+
+ Your Validators
+
+
+
+ {validatorsLoading ? (
+
+
+
+
+ ) : !signerInstalled || !brainRunning ? (
+
+
+
+ {!signerInstalled
+ ? "Web3Signer is not installed on this network."
+ : "Web3Signer is not running properly on this network."}
+
+
+ Select Web3Signer in the stakers tab and apply changes.
+
+
+ ) : (
+ <>
+
+
+
Total
+ {data?.beaconError && (
+
+
+
+
+
+
+ Error fetching {capitalize(network)} validators status. All keystores imported in your{" "}
+ {capitalize(network)} Web3Signer are being considered as active validators.
+
+
+
+ )}
+
+
{data?.total ?? "0"}
+
+
+
+
+
+
+
+ Balance
+
+ {typeof data?.balance === "number" || typeof data?.balance === "string"
+ ? gweiToToken(data.balance, network as Network)
+ : "-"}
+
+
+ >
+ )}
+
+
+ {!signerInstalled || !brainRunning ? (
+ navigate(withLegacyBase(`stakers/${network === Network.Mainnet ? "ethereum" : network}`))}
+ >
+ Set Web3Signer
+
+ ) : (
+
+
+ {(data?.total ?? 0) < 1 ? "Import Validators" : "Manage Validators"}
+
+
+
+ )}
+
+
+ );
+}
+
+function AttestingBadge({ attesting, total }: { attesting: number; total: number }) {
+ if (total === 0) return - ;
+ if (attesting === total) return Online ;
+ if (attesting === 0) return Offline ;
+ return (
+
+ {attesting}/{total}
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/dashboard/index.ts b/packages/admin-ui/src/pages-new/staking/dashboard/index.ts
new file mode 100644
index 0000000000..a66b98a00f
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/dashboard/index.ts
@@ -0,0 +1 @@
+export { DashboardPage } from "./DashboardPage";
diff --git a/packages/admin-ui/src/pages-new/staking/data.ts b/packages/admin-ui/src/pages-new/staking/data.ts
new file mode 100644
index 0000000000..f387536cf2
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/data.ts
@@ -0,0 +1 @@
+export const stakingBasePath = "/staking";
diff --git a/packages/admin-ui/src/pages-new/staking/packagesConfig.ts b/packages/admin-ui/src/pages-new/staking/packagesConfig.ts
new file mode 100644
index 0000000000..4727faeb95
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/packagesConfig.ts
@@ -0,0 +1,9 @@
+import { PackagesConfig } from "pages-new/packages/config";
+
+export const stakingPackagesConfig: PackagesConfig = {
+ sectionLabel: "Staking",
+ categoryFilter: { mode: "exclude", categories: ["AI"] },
+ packagesPath: "/staking/packages",
+ storePath: "/staking/store",
+ installerPath: "/installer"
+};
diff --git a/packages/admin-ui/src/pages-new/staking/stakers/ClientList.tsx b/packages/admin-ui/src/pages-new/staking/stakers/ClientList.tsx
new file mode 100644
index 0000000000..b12dc1318e
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/stakers/ClientList.tsx
@@ -0,0 +1,174 @@
+import React from "react";
+import { StakerItem, StakerItemOk } from "@dappnode/types";
+import { RadioGroup, RadioGroupItem } from "components/primitives/radio-group";
+import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions } from "components/primitives/item";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { cn } from "lib/utils";
+import { prettyDnpName } from "utils/format";
+import { useNavigate } from "react-router-dom";
+import { CircleAlert, ArrowUpCircle, KeyRound } from "lucide-react";
+
+/* ── Unselect sentinel ──────────────────────────────────────────────── */
+
+/** RadioGroup doesn't natively support deselect — we use a hidden "none" value */
+const NONE_VALUE = "__none__";
+
+/* ── Props ──────────────────────────────────────────────────────────── */
+
+interface ClientListProps {
+ clients: StakerItem[];
+ selectedDnpName: string | null;
+ onSelect: (client: StakerItemOk) => void;
+ onDeselect: () => void;
+ isDisabled?: boolean;
+}
+
+/**
+ * Radio-group list of clients. Exactly one can be selected at a time,
+ * or none if the user clicks the already-selected item.
+ */
+export function ClientList({ clients, selectedDnpName, onSelect, onDeselect, isDisabled }: ClientListProps) {
+ const handleValueChange = (value: string) => {
+ if (value === NONE_VALUE) return;
+ if (value === selectedDnpName) {
+ onDeselect();
+ return;
+ }
+ const match = clients.find((c) => c.dnpName === value);
+ if (match && match.status === "ok") onSelect(match);
+ };
+
+ return (
+
+ {clients.map((client) => (
+ {
+ if (client.status !== "ok") return;
+ client.dnpName === selectedDnpName ? onDeselect() : onSelect(client as StakerItemOk);
+ }}
+ />
+ ))}
+
+ );
+}
+
+/* ── Single row ─────────────────────────────────────────────────────── */
+
+function ClientRow({
+ client,
+ isSelected,
+ isDisabled,
+ onToggle
+}: {
+ client: StakerItem;
+ isSelected: boolean;
+ isDisabled?: boolean;
+ onToggle: () => void;
+}) {
+ const navigate = useNavigate();
+
+ const isError = client.status === "error";
+ const isOk = client.status === "ok";
+ const showUpdate = isOk && isSelected && client.isInstalled && !client.isUpdated;
+ const keystoresUrl = isOk && isSelected && client.isInstalled ? client.data?.manifest?.links?.ui : undefined;
+
+ return (
+ -
+ {/* Radio indicator */}
+
e.stopPropagation()}
+ />
+
+ {/* Avatar */}
+
+ {isOk ? (
+ {
+ (e.target as HTMLImageElement).src = "/assets/defaultAvatar.png";
+ }}
+ />
+ ) : (
+
+
+
+ )}
+
+
+ {/* Text */}
+
+
+ {prettyDnpName(client.dnpName)}
+ {isError && (
+
+ Error
+
+ )}
+ {isOk && isSelected && client.isInstalled && client.isRunning && (
+
+ Running
+
+ )}
+
+ {isOk && client.data?.manifest?.shortDescription && (
+ {client.data.manifest.shortDescription}
+ )}
+
+
+ {/* Actions */}
+
+ {keystoresUrl && (
+ e.stopPropagation()}
+ >
+
+
+ Keystores
+
+
+ )}
+ {showUpdate && (
+ {
+ e.stopPropagation();
+ navigate(`/installer/${client.dnpName}`);
+ }}
+ >
+
+ Update
+
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/stakers/MevBoostSection.tsx b/packages/admin-ui/src/pages-new/staking/stakers/MevBoostSection.tsx
new file mode 100644
index 0000000000..813e15ba93
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/stakers/MevBoostSection.tsx
@@ -0,0 +1,225 @@
+import React, { useState } from "react";
+import { Network, StakerItem, StakerItemOk } from "@dappnode/types";
+import {
+ Item,
+ ItemMedia,
+ ItemContent,
+ ItemTitle,
+ ItemDescription,
+ ItemActions,
+ ItemGroup
+} from "components/primitives/item";
+import { Checkbox } from "components/primitives/checkbox";
+import { Button } from "components/primitives/button";
+import { Badge } from "components/primitives/badge";
+import { Switch } from "components/primitives/switch";
+import { Separator } from "components/primitives/separator";
+import { cn } from "lib/utils";
+import { prettyDnpName } from "utils/format";
+import { useNavigate } from "react-router-dom";
+import { CircleAlert, ArrowUpCircle, Info, ExternalLink } from "lucide-react";
+import { getDefaultRelays, RelayDef } from "./data";
+
+/* ── Props ──────────────────────────────────────────────────────────── */
+
+interface MevBoostSectionProps {
+ network: Network;
+ mevBoost: StakerItem;
+ newMevBoost: StakerItemOk | null;
+ setNewMevBoost: React.Dispatch>;
+ newRelays: string[];
+ setNewRelays: React.Dispatch>;
+ isSelected: boolean;
+ isDisabled?: boolean;
+}
+
+/**
+ * MEV Boost toggle as a checkbox Item row, with an expandable relays list below.
+ */
+export function MevBoostSection({
+ network,
+ mevBoost,
+ newMevBoost,
+ setNewMevBoost,
+ newRelays,
+ setNewRelays,
+ isSelected,
+ isDisabled
+}: MevBoostSectionProps) {
+ const navigate = useNavigate();
+
+ const isOk = mevBoost.status === "ok";
+ const isError = mevBoost.status === "error";
+ const showUpdate = isOk && isSelected && mevBoost.isInstalled && !mevBoost.isUpdated;
+
+ const handleToggle = () => {
+ if (isDisabled || !isOk) return;
+ if (isSelected) {
+ setNewMevBoost(null);
+ } else {
+ setNewMevBoost(mevBoost as StakerItemOk);
+ }
+ };
+
+ return (
+
+ {/* Main toggle row */}
+
-
+
handleToggle()}
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+ {isOk ? (
+ {
+ (e.target as HTMLImageElement).src = "/assets/defaultAvatar.png";
+ }}
+ />
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {prettyDnpName(mevBoost.dnpName)}
+ {isError && (
+
+ Error
+
+ )}
+
+ {isOk && mevBoost.data?.manifest?.shortDescription && (
+ {mevBoost.data.manifest.shortDescription}
+ )}
+
+
+
+ {showUpdate && (
+ {
+ e.stopPropagation();
+ navigate(`/installer/${mevBoost.dnpName}`);
+ }}
+ >
+
+ Update
+
+ )}
+
+
+
+ {/* Relay list — only when selected */}
+ {newMevBoost?.status === "ok" && isSelected && (
+
+ )}
+
+ );
+}
+
+/* ── Relays ──────────────────────────────────────────────────────────── */
+
+function RelaysList({
+ network,
+ newRelays,
+ setNewRelays
+}: {
+ network: Network;
+ newRelays: string[];
+ setNewRelays: React.Dispatch>;
+}) {
+ const relays = getDefaultRelays(network);
+ if (relays.length === 0) return null;
+
+ return (
+
+
+
+
+ {relays.map((relay, i) => (
+
+
+ {i < relays.length - 1 && }
+
+ ))}
+
+
+ );
+}
+
+function RelayRow({
+ relay,
+ newRelays,
+ setNewRelays
+}: {
+ relay: RelayDef;
+ newRelays: string[];
+ setNewRelays: React.Dispatch>;
+}) {
+ const [isAdded, setIsAdded] = useState(newRelays.includes(relay.url));
+
+ const toggle = () => {
+ if (isAdded) {
+ setNewRelays(newRelays.filter((r) => r !== relay.url));
+ setIsAdded(false);
+ } else {
+ setNewRelays([...newRelays, relay.url]);
+ setIsAdded(true);
+ }
+ };
+
+ return (
+ -
+
+
+ {relay.docs ? (
+
+ {relay.operator}
+
+
+ ) : (
+ relay.operator
+ )}
+ {relay.ofacCompliant !== undefined && (
+
+ {relay.ofacCompliant ? "OFAC" : "Non-OFAC"}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/stakers/StakerNetworkConfig.tsx b/packages/admin-ui/src/pages-new/staking/stakers/StakerNetworkConfig.tsx
new file mode 100644
index 0000000000..b4c411c55d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/stakers/StakerNetworkConfig.tsx
@@ -0,0 +1,286 @@
+import React from "react";
+import { api, useApi } from "api";
+import { Network } from "@dappnode/types";
+import { Skeleton } from "components/primitives/skeleton";
+import { Button } from "components/primitives/button";
+import { Alert, AlertDescription } from "components/primitives/alert";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "components/primitives/collapsible";
+import { TypographyH4 } from "components/primitives/typography";
+import { toast } from "sonner";
+import { ChevronDown, AlertTriangle, Info } from "lucide-react";
+import { useStakerConfig } from "hooks/stakers/useStakerConfig";
+import { useStakerModals } from "hooks/stakers/useStakerModals";
+import {
+ UpgradeToPremiumModal,
+ ActivateBackupModal,
+ StakerDisclaimerModal
+} from "components/modals/StakersPremiumModals";
+import { NetworkDef, mevBoostNetworks, smoothNetworks } from "./data";
+import { ClientList } from "./ClientList";
+import { MevBoostSection } from "./MevBoostSection";
+
+interface StakerNetworkConfigProps {
+ networkDef: NetworkDef;
+}
+
+/**
+ * Per-network staker configuration panel.
+ * Shows execution clients, consensus clients, remote signer, and MEV boost columns.
+ */
+export function StakerNetworkConfig({ networkDef }: StakerNetworkConfigProps) {
+ const { network, description } = networkDef;
+ const currentStakerConfigReq = useApi.stakerConfigGet({ network });
+
+ const {
+ reqStatus,
+ setReqStatus,
+ newExecClient,
+ setNewExecClient,
+ newConsClient,
+ setNewConsClient,
+ newMevBoost,
+ setNewMevBoost,
+ newRelays,
+ setNewRelays,
+ newWeb3signer,
+ setNewWeb3signer,
+ changes
+ } = useStakerConfig(network, currentStakerConfigReq);
+
+ const isExecutionChanged =
+ newExecClient?.dnpName !==
+ currentStakerConfigReq.data?.executionClients.find((ec) => ec.status === "ok" && ec.isSelected)?.dnpName;
+ const isSignerSelected = Boolean(newWeb3signer?.isSelected);
+
+ const {
+ nonPremiumModalShow,
+ nonPremiumModalOnClose,
+ premiumModalShow,
+ premiumModalOnClose,
+ disclaimerModalShow,
+ disclaimerModalOnClose,
+ displayPremiumModals,
+ displayDisclaimerModal
+ } = useStakerModals({ network, isExecutionChanged, isSignerSelected });
+
+ /* ── Apply changes ──────────────────────────────────────────────── */
+
+ async function setNewConfig() {
+ let showToast = false;
+ try {
+ if (!changes.isAllowed) return;
+
+ const userApproved = await displayDisclaimerModal();
+ if (!userApproved) return;
+
+ setReqStatus({ loading: true });
+ displayPremiumModals();
+
+ const toastId = toast.loading("Setting new staker configuration…");
+ await api.stakerConfigSet({
+ stakerConfig: {
+ network,
+ executionDnpName: newExecClient?.dnpName || null,
+ consensusDnpName: newConsClient?.dnpName || null,
+ mevBoostDnpName: newMevBoost?.dnpName || null,
+ web3signerDnpName: newWeb3signer?.dnpName || null,
+ relays: newRelays
+ }
+ });
+ toast.success("Successfully set new staker configuration", { id: toastId });
+ setReqStatus({ result: true });
+ showToast = true;
+ } catch (e) {
+ toast.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
+ setReqStatus({ error: e });
+ showToast = true;
+ } finally {
+ if (showToast) {
+ setReqStatus({ loading: true });
+ const reloadId = toast.loading(`Reloading ${network} staker configuration…`);
+ try {
+ await currentStakerConfigReq.revalidate();
+ toast.success(`Loaded ${network} staker configuration`, { id: reloadId });
+ } catch (e) {
+ toast.error(`Error reloading: ${e instanceof Error ? e.message : String(e)}`, { id: reloadId });
+ }
+ setReqStatus({ loading: false });
+ }
+ }
+ }
+
+ /* ── Loading / error states ─────────────────────────────────────── */
+
+ if (currentStakerConfigReq.isValidating && !currentStakerConfigReq.data) {
+ return (
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+ );
+ }
+
+ if (currentStakerConfigReq.error) {
+ return (
+
+
+
+ Error loading {network} configuration: {currentStakerConfigReq.error.message}
+
+
+ );
+ }
+
+ const data = currentStakerConfigReq.data;
+ if (!data) return null;
+
+ const showMevBoost = mevBoostNetworks.includes(network) && data.mevBoost;
+ const showSigner = network !== Network.Sepolia && data.web3Signer;
+
+ return (
+
+ {/* Modals (reuse existing legacy modal components) */}
+
+
+
+
+ {/* Info banners */}
+ {network === Network.Sepolia && (
+
+
+
+ Sepolia network is not intended for staking , as it only supports whitelisted validators.
+ Running a Sepolia node is still useful for L2s, infrastructure testing, and other use cases.
+
+
+ )}
+
+ {smoothNetworks.includes(network) && (
+
+
+
+ Smooth is out! Discover the new MEV Smoothing Pool designed for solo validators.{" "}
+
+ Learn more
+
+
+
+ )}
+
+ {/* Network description (collapsible) */}
+
+
+
+ Network Description
+
+
+ {description}
+
+ Set up your Proof-of-Stake validator configuration:
+ (1) Choose an Execution Layer client
+ (2) Choose a Consensus Layer client (+ validator)
+ {network !== Network.Sepolia && (
+ <>
+
+ (3) Install the web3signer, which will hold the validator keys and sign
+ {showMevBoost && (
+ <>
+
+ (4) Optional; delegate block-building capacities through the MEV Boost network
+ >
+ )}
+ >
+ )}
+
+
+
+
+ {/* ── Client sections ─────────────────────────────────────────── */}
+
+ {/* Execution Clients */}
+
+ Execution Clients
+ setNewExecClient(ec)}
+ onDeselect={() => setNewExecClient(null)}
+ isDisabled={reqStatus.loading}
+ />
+
+
+ {/* Consensus Clients */}
+ {data.consensusClients.length > 0 && (
+
+ Consensus Clients
+ setNewConsClient(cc)}
+ onDeselect={() => setNewConsClient(null)}
+ isDisabled={reqStatus.loading}
+ />
+
+ )}
+
+ {/* Remote Signer */}
+ {showSigner && (
+
+ Remote Signer
+ setNewWeb3signer(s)}
+ onDeselect={() => setNewWeb3signer(null)}
+ isDisabled={reqStatus.loading}
+ />
+
+ )}
+
+ {/* MEV Boost */}
+ {showMevBoost && data.mevBoost && (
+
+ MEV Boost
+
+
+ )}
+
+
+ {/* ── Apply button & validation ────────────────────────────── */}
+
+
+
+ Apply changes
+
+
+
+ {!changes.isAllowed && changes.reason && (
+
+
+
+ Cannot apply changes: {changes.reason}
+
+
+ )}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/stakers/StakersPage.tsx b/packages/admin-ui/src/pages-new/staking/stakers/StakersPage.tsx
new file mode 100644
index 0000000000..b8b25d2ddb
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/stakers/StakersPage.tsx
@@ -0,0 +1,107 @@
+import React, { useMemo } from "react";
+import { Routes, Route, Navigate, NavLink, useLocation, useNavigate } from "react-router-dom";
+import { PageContainer, PageHeader } from "components/primitives/page";
+import { Switch } from "components/primitives/switch";
+import { Label } from "components/primitives/label";
+import {
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuLink
+} from "components/primitives/navigation-menu";
+import { cn } from "lib/utils";
+import { networkDefs } from "./data";
+import { StakerNetworkConfig } from "./StakerNetworkConfig";
+
+/**
+ * Main Stakers page — replaces the legacy StakersRoot.
+ * Provides a horizontal network tab bar with a mainnet/testnet toggle.
+ */
+export function StakersPage() {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ // Determine current slug from URL
+ const currentSlug = location.pathname.split("/").pop() || "";
+ const currentDef = networkDefs.find((d) => d.slug === currentSlug);
+
+ // Derive testnet toggle from current selection
+ const testnetsSelected = currentDef ? currentDef.group === "testnet" : false;
+
+ // Filter visible networks based on the toggle
+ const filteredNetworks = useMemo(
+ () => networkDefs.filter((d) => (testnetsSelected ? d.group === "testnet" : d.group === "mainnet")),
+ [testnetsSelected]
+ );
+
+ // Handle mainnet/testnet toggle
+ const handleToggle = (toTestnets: boolean) => {
+ const target = networkDefs.find((d) => d.group === (toTestnets ? "testnet" : "mainnet"));
+ if (target) {
+ navigate(target.slug);
+ }
+ };
+
+ return (
+
+
+
+ {/* Toggle + Network tabs */}
+
+
+
+ Mainnet
+
+
+
+ Testnet
+
+
+
+
+
+ {filteredNetworks.map((def) => (
+
+
+
+ cn(
+ "tw:inline-flex tw:h-9 tw:items-center tw:justify-center tw:rounded-lg tw:px-3 tw:py-1.5 tw:text-sm tw:font-medium tw:transition-colors",
+ isActive
+ ? "tw:bg-primary/10 tw:text-primary"
+ : "tw:text-muted-foreground tw:hover:bg-muted tw:hover:text-foreground"
+ )
+ }
+ >
+ {def.label}
+
+
+
+ ))}
+
+
+
+
+ {/* Network config routes */}
+
+ } />
+ {networkDefs
+ .filter((d) => d.type === "staker")
+ .map((def) => (
+ } />
+ ))}
+ {/* Starknet / Optimism — placeholder for now, these have custom config UIs */}
+ {networkDefs
+ .filter((d) => d.type !== "staker")
+ .map((def) => (
+ }
+ />
+ ))}
+
+
+ );
+}
diff --git a/packages/admin-ui/src/pages-new/staking/stakers/data.ts b/packages/admin-ui/src/pages-new/staking/stakers/data.ts
new file mode 100644
index 0000000000..5b655423da
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/stakers/data.ts
@@ -0,0 +1,244 @@
+import { Network } from "@dappnode/types";
+
+/* ── Network definitions ────────────────────────────────────────────── */
+
+export interface NetworkDef {
+ /** URL-friendly slug used in the route */
+ slug: string;
+ /** Human-readable label */
+ label: string;
+ /** Network enum value */
+ network: Network;
+ /** Short description shown in the accordion */
+ description: string;
+ /** "mainnet" | "testnet" for the toggle filter */
+ group: "mainnet" | "testnet";
+ /** Network type for deciding which columns to show */
+ type: "staker" | "starknet" | "optimism";
+}
+
+export const networkDefs: NetworkDef[] = [
+ {
+ slug: "ethereum",
+ label: "Ethereum",
+ network: Network.Mainnet,
+ group: "mainnet",
+ type: "staker",
+ description:
+ "Ethereum is a decentralized, permissionless blockchain platform that employs a proof-of-stake (PoS) consensus mechanism, where validators stake Ether (ETH) to secure the network and validate transactions in exchange for ETH rewards."
+ },
+ {
+ slug: "gnosis",
+ label: "Gnosis",
+ network: Network.Gnosis,
+ group: "mainnet",
+ type: "staker",
+ description:
+ "The Gnosis Chain Network is a highly efficient, Ethereum-compatible blockchain that emphasizes security, low transaction costs, and fast execution speeds, catering primarily to decentralized finance (DeFi) applications and prediction markets."
+ },
+ {
+ slug: "lukso",
+ label: "Lukso",
+ network: Network.Lukso,
+ group: "mainnet",
+ type: "staker",
+ description:
+ "The LUKSO Blockchain is a next-gen, Ethereum-based platform designed specifically for the fashion, gaming, design, and social media industries, focusing on creating a new digital lifestyle space."
+ },
+ {
+ slug: "hoodi",
+ label: "Hoodi",
+ network: Network.Hoodi,
+ group: "testnet",
+ type: "staker",
+ description:
+ "Hoodi is the latest public Ethereum testnet introduced to support the Pectra upgrade. It focuses on testing Ethereum Improvement Proposals (EIPs), staking mechanisms, and wallet interactions in a post-merge environment."
+ },
+ {
+ slug: "sepolia",
+ label: "Sepolia",
+ network: Network.Sepolia,
+ group: "testnet",
+ type: "staker",
+ description:
+ "Sepolia is a public Ethereum testnet designed for developers who want to test their applications in a pre-production environment. While not a network for testing staking, it is still a valuable resource for developers."
+ },
+ {
+ slug: "starknet",
+ label: "Starknet",
+ network: Network.StarknetMainnet,
+ group: "mainnet",
+ type: "starknet",
+ description:
+ "Starknet is a permissionless decentralized Layer 2 validity rollup (ZK-Rollup) that operates as a scaling solution for Ethereum, leveraging STARK cryptographic proofs."
+ },
+ {
+ slug: "starknet-sepolia",
+ label: "Starknet Sepolia",
+ network: Network.StarknetSepolia,
+ group: "testnet",
+ type: "starknet",
+ description:
+ "Starknet Sepolia is the public testnet for Starknet, providing developers with a pre-production environment to test their applications."
+ },
+ {
+ slug: "optimism",
+ label: "Optimism",
+ network: Network.Mainnet, // Optimism uses mainnet as L1
+ group: "mainnet",
+ type: "optimism",
+ description:
+ "Optimism is a Layer 2 solution for Ethereum. Rather than operating as an independent EVM chain, Optimism executes transactions off-chain and posts compressed data to Ethereum."
+ }
+];
+
+/* ── Relay definitions ──────────────────────────────────────────────── */
+
+export interface RelayDef {
+ operator: string;
+ url: string;
+ docs?: string;
+ ofacCompliant?: boolean;
+}
+
+export function getDefaultRelays(network: Network): RelayDef[] {
+ switch (network) {
+ case "mainnet":
+ return [
+ {
+ operator: "Agnostic Boost",
+ ofacCompliant: false,
+ docs: "https://agnostic-relay.net/",
+ url: "https://0xa7ab7a996c8584251c8f925da3170bdfd6ebc75d50f5ddc4050a6fdc77f2a3b5fce2cc750d0865e05d7228af97d69561@agnostic-relay.net"
+ },
+ {
+ operator: "Ultra Sound",
+ ofacCompliant: false,
+ docs: "https://relay.ultrasound.money/",
+ url: "https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.money"
+ },
+ {
+ operator: "Ultra Sound (filtered)",
+ ofacCompliant: true,
+ docs: "https://relay.ultrasound.money/",
+ url: "https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay-filtered.ultrasound.money"
+ },
+ {
+ operator: "Flashbots",
+ ofacCompliant: true,
+ docs: "https://boost.flashbots.net/",
+ url: "https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net"
+ },
+ {
+ operator: "bloXroute (Max profit)",
+ ofacCompliant: true,
+ docs: "https://bloxroute.com/",
+ url: "https://0x8b5d2e73e2a3a55c6c87b8b6eb92e0149a125c852751db1422fa951e42a09b82c142c3ea98d0d9930b056a3bc9896b8f@bloxroute.max-profit.blxrbdn.com"
+ },
+ {
+ operator: "bloXroute (Regulated)",
+ ofacCompliant: true,
+ docs: "https://bloxroute.com/",
+ url: "https://0xb0b07cd0abef743db4260b0ed50619cf6ad4d82064cb4fbec9d3ec530f7c5e6793d9f286c4e082c0244ffb9f2658fe88@bloxroute.regulated.blxrbdn.com"
+ },
+ {
+ operator: "Titan (Non-Filtered)",
+ ofacCompliant: false,
+ docs: "https://docs.titanrelay.xyz/",
+ url: "https://0x8c4ed5e24fe5c6ae21018437bde147693f68cda427cd1122cf20819c30eda7ed74f72dece09bb313f2a1855595ab677d@global.titanrelay.xyz"
+ },
+ {
+ operator: "Titan (Filtered)",
+ ofacCompliant: true,
+ docs: "https://docs.titanrelay.xyz/",
+ url: "https://0x8c4ed5e24fe5c6ae21018437bde147693f68cda427cd1122cf20819c30eda7ed74f72dece09bb313f2a1855595ab677d@regional.titanrelay.xyz"
+ },
+ {
+ operator: "Aestus",
+ ofacCompliant: false,
+ docs: "https://aestus.live/",
+ url: "https://0xa15b52576bcbf1072f4a011c0f99f9fb6c66f3e1ff321f11f461d15e31b1cb359caa092c71bbded0bae5b5ea401aab7e@aestus.live"
+ }
+ ];
+ case "holesky":
+ return [
+ {
+ operator: "Flashbots",
+ docs: "https://www.flashbots.net/",
+ url: "https://0xafa4c6985aa049fb79dd37010438cfebeb0f2bd42b115b89dd678dab0670c1de38da0c4e9138c9290a398ecd9a0b3110@boost-relay-holesky.flashbots.net"
+ },
+ {
+ operator: "Aestus",
+ docs: "https://flashbots.notion.site/Relay-API-Documentation-5fb0819366954962bc02e81cb33840f5#417abe417dde45caaff3dc15aaae65dd",
+ url: "https://0xab78bf8c781c58078c3beb5710c57940874dd96aef2835e7742c866b4c7c0406754376c2c8285a36c630346aa5c5f833@holesky.aestus.live"
+ },
+ {
+ operator: "Ultrasound",
+ docs: "https://github.com/ultrasoundmoney/frontend",
+ url: "https://0xb1559beef7b5ba3127485bbbb090362d9f497ba64e177ee2c8e7db74746306efad687f2cf8574e38d70067d40ef136dc@relay-stag.ultrasound.money"
+ },
+ {
+ operator: "Titan",
+ docs: "https://docs.titanrelay.xyz/",
+ url: "https://0xaa58208899c6105603b74396734a6263cc7d947f444f396a90f7b7d3e65d102aec7e5e5291b27e08d02c50a050825c2f@holesky.titanrelay.xyz"
+ },
+ {
+ operator: "bloXroute",
+ docs: "https://bloxroute.holesky.blxrbdn.com/",
+ url: "https://0x821f2a65afb70e7f2e820a925a9b4c80a159620582c1766b1b09729fec178b11ea22abb3a51f07b288be815a1a2ff516@bloxroute.holesky.blxrbdn.com"
+ }
+ ];
+ case "hoodi":
+ return [
+ {
+ operator: "Flashbots",
+ docs: "https://www.flashbots.net/",
+ url: "https://0xafa4c6985aa049fb79dd37010438cfebeb0f2bd42b115b89dd678dab0670c1de38da0c4e9138c9290a398ecd9a0b3110@boost-relay-hoodi.flashbots.net"
+ },
+ {
+ operator: "bloXroute",
+ docs: "https://bloxroute.holesky.blxrbdn.com/",
+ url: "https://0x821f2a65afb70e7f2e820a925a9b4c80a159620582c1766b1b09729fec178b11ea22abb3a51f07b288be815a1a2ff516@bloxroute.hoodi.blxrbdn.com"
+ },
+ {
+ operator: "Titan",
+ docs: "https://docs.titanrelay.xyz/",
+ url: "https://0xaa58208899c6105603b74396734a6263cc7d947f444f396a90f7b7d3e65d102aec7e5e5291b27e08d02c50a050825c2f@hoodi.titanrelay.xyz"
+ },
+ {
+ operator: "Aestus",
+ docs: "https://flashbots.notion.site/Relay-API-Documentation-5fb0819366954962bc02e81cb33840f5#417abe417dde45caaff3dc15aaae65dd",
+ url: "https://0x98f0ef62f00780cf8eb06701a7d22725b9437d4768bb19b363e882ae87129945ec206ec2dc16933f31d983f8225772b6@hoodi.aestus.live"
+ }
+ ];
+ case "prater":
+ return [
+ {
+ operator: "Flashbots",
+ docs: "https://www.flashbots.net/",
+ url: "https://0xafa4c6985aa049fb79dd37010438cfebeb0f2bd42b115b89dd678dab0670c1de38da0c4e9138c9290a398ecd9a0b3110@builder-relay-goerli.flashbots.net"
+ },
+ {
+ operator: "bloXroute",
+ docs: "https://bloxroute.com/",
+ url: "https://0x821f2a65afb70e7f2e820a925a9b4c80a159620582c1766b1b09729fec178b11ea22abb3a51f07b288be815a1a2ff516@bloxroute.max-profit.builder.goerli.blxrbdn.com"
+ }
+ ];
+ default:
+ return [];
+ }
+}
+
+/** Networks that support MEV boost relays */
+export const mevBoostNetworks: Network[] = [
+ Network.Mainnet,
+ Network.Prater,
+ Network.Holesky,
+ Network.Hoodi
+];
+
+/** Networks that show the Smooth promo banner */
+export const smoothNetworks: Network[] = [Network.Hoodi, Network.Mainnet];
+
+/** Networks where Sepolia staking note is shown */
+export const sepoliaStakingNote = Network.Sepolia;
diff --git a/packages/admin-ui/src/pages-new/staking/stakers/index.ts b/packages/admin-ui/src/pages-new/staking/stakers/index.ts
new file mode 100644
index 0000000000..c1cb98edbd
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/staking/stakers/index.ts
@@ -0,0 +1 @@
+export { StakersPage } from "./StakersPage";
diff --git a/packages/admin-ui/src/pages-new/utils/actions.ts b/packages/admin-ui/src/pages-new/utils/actions.ts
new file mode 100644
index 0000000000..4488b6f2d7
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/utils/actions.ts
@@ -0,0 +1,44 @@
+import { api } from "api";
+import { toast } from "sonner";
+
+/**
+ * Remove an orphan volume by name.
+ * Confirmation is handled by the caller via AlertDialog.
+ */
+export async function volumeRemove(name: string): Promise {
+ const toastId = toast.loading("Removing volume...");
+ try {
+ await api.volumeRemove({ name });
+ toast.success("Removed volume", { id: toastId });
+ } catch (e) {
+ toast.error(`Error removing volume: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+}
+
+/**
+ * Remove a package-owned volume.
+ * Confirmation is handled by the caller via AlertDialog.
+ */
+export async function packageVolumeRemove(dnpName: string, volName: string): Promise {
+ const toastId = toast.loading("Removing volume...");
+ try {
+ await api.packageRestartVolumes({ dnpName, volumeId: volName });
+ toast.success("Removed volume", { id: toastId });
+ } catch (e) {
+ toast.error(`Error removing volume: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+}
+
+/**
+ * Change the host user password.
+ * Confirmation is handled by the caller via AlertDialog.
+ */
+export async function passwordChange(newPassword: string): Promise {
+ const toastId = toast.loading("Changing host user password...");
+ try {
+ await api.passwordChange({ newPassword });
+ toast.success("Changed host user password", { id: toastId });
+ } catch (e) {
+ toast.error(`Error changing password: ${e instanceof Error ? e.message : String(e)}`, { id: toastId });
+ }
+}
diff --git a/packages/admin-ui/src/pages-new/utils/autoDiagnoseTexts.ts b/packages/admin-ui/src/pages-new/utils/autoDiagnoseTexts.ts
new file mode 100644
index 0000000000..a8a085d745
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/utils/autoDiagnoseTexts.ts
@@ -0,0 +1,166 @@
+import { SystemInfo, InstalledPackageData, HostStatDisk, PublicIpResponse } from "@dappnode/types";
+import { docsUrl, mandatoryCoreDnps } from "params";
+import { responseInterface } from "swr";
+
+type DiagnoseResult = {
+ loading?: boolean;
+ ok?: boolean;
+ msg: string;
+ solutions?: string[];
+ link?: { linkMsg: string; linkUrl: string };
+};
+
+type DiagnoseResultOrNull = DiagnoseResult | null;
+
+export function connection({ isOpen, error }: { isOpen: boolean; error: string | null }): DiagnoseResultOrNull {
+ return {
+ ok: isOpen,
+ msg: isOpen ? "Session is open" : `Session is closed: ${error || ""}`,
+ solutions: [
+ `You may be disconnected from your DAppNode's VPN. Please make sure your connection is still active`,
+ `If you are still connected, disconnect your VPN connection, connect again and refresh this page`
+ ]
+ };
+}
+
+export function internetConnection(
+ publicIpRes: responseInterface,
+ systemInfo: responseInterface
+): DiagnoseResultOrNull {
+ if (publicIpRes.isValidating) return { loading: true, msg: "Fetching public IP..." };
+
+ if (publicIpRes.data?.publicIp) {
+ return {
+ ok: true,
+ msg: "Has connected to the internet, and detected own public IP"
+ };
+ } else {
+ const msgs: string[] = ["Cannot connect to the internet", "Could not fetch own public IP"];
+ if (systemInfo.data?.publicIp) {
+ msgs.push("Was previously connected but got disconnected");
+ }
+ if (publicIpRes.error?.message) {
+ msgs.push(publicIpRes.error?.message);
+ }
+ return {
+ ok: false,
+ msg: msgs.join(". "),
+ solutions: [
+ "Make sure your DAppNode is connected to the internet. Make sure to plug its ethernet cable to the router."
+ ],
+ link: {
+ linkMsg: "Learn more about it in our Documentation!",
+ linkUrl: docsUrl.connectToRouter
+ }
+ };
+ }
+}
+
+export function openPorts({
+ data: dappnodeParams,
+ isValidating
+}: {
+ data?: SystemInfo;
+ isValidating: boolean;
+}): DiagnoseResultOrNull {
+ if (isValidating) return { loading: true, msg: "Loading system info..." };
+ if (!dappnodeParams) return null;
+ const { alertToOpenPorts } = dappnodeParams;
+ return {
+ ok: !alertToOpenPorts,
+ msg: alertToOpenPorts
+ ? "Ports have to be opened and there is no UPnP device available"
+ : "No ports have to be opened OR the router has UPnP enabled",
+ solutions: [
+ "If you are capable of opening ports manually, please ignore this error",
+ "Your router may have UPnP but it is not turned on yet. Please research if your specific router has UPnP and turn it on"
+ ]
+ };
+}
+
+export function noNatLoopback({
+ data: dappnodeParams,
+ isValidating
+}: {
+ data?: SystemInfo;
+ isValidating: boolean;
+}): DiagnoseResultOrNull {
+ if (isValidating) return { loading: true, msg: "Loading system info..." };
+ if (!dappnodeParams) return null;
+ const { noNatLoopback, internalIp } = dappnodeParams;
+ return {
+ ok: !noNatLoopback,
+ msg: noNatLoopback ? "No NAT loopback, external IP did not resolve" : "NAT loopback enabled, external IP resolves",
+ solutions: [`Please use the internal IP: ${internalIp} when you are in the same network as your DAppNode`]
+ };
+}
+
+export function ipfs(ipfsTestRes: responseInterface): DiagnoseResultOrNull {
+ if (ipfsTestRes.isValidating) return { loading: true, msg: "Checking if IPFS resolves..." };
+ const error = ipfsTestRes.error;
+ return {
+ ok: !error,
+ msg: error ? "IPFS is not resolving: " + error.message : "IPFS resolves",
+ solutions: [
+ `Go to the system tab and make sure IPFS is running. Otherwise open the package and click 'restart'`,
+ `If the problem persist make sure your disk has not run of space; IPFS may malfunction in that case.`
+ ]
+ };
+}
+
+export function diskSpace({
+ data,
+ isValidating
+}: {
+ data?: HostStatDisk;
+ isValidating: boolean;
+}): DiagnoseResultOrNull {
+ if (isValidating) return { loading: true, msg: "Checking disk usage..." };
+ if (!data || !data.usedPercentage) return null;
+ const ok = data.usedPercentage < 95;
+ return {
+ ok,
+ msg: ok ? "Disk usage is ok (<95%)" : "Disk usage is over 95%",
+ solutions: [
+ "If the disk usage gets to 100%, DAppNode will stop working. Please empty some disk space",
+ "Locate DAppNode Packages with big volumes such as blockchain nodes and remove their data"
+ ]
+ };
+}
+
+export function coreDnpsRunning({
+ data: dnpInstalled,
+ isValidating
+}: {
+ data?: InstalledPackageData[];
+ isValidating: boolean;
+}): DiagnoseResultOrNull {
+ if (isValidating)
+ return {
+ loading: true,
+ msg: "Verifying installed core DAppNode Packages..."
+ };
+
+ if (!dnpInstalled) return null;
+
+ const notFound: string[] = [];
+ const notRunning: string[] = [];
+ for (const coreDnpName of mandatoryCoreDnps) {
+ const coreDnp = dnpInstalled.find((dnp) => dnp.dnpName === coreDnpName);
+ if (!coreDnp) notFound.push(coreDnpName);
+ else if (coreDnp.containers.some((container) => !container.running)) notRunning.push(coreDnpName);
+ }
+
+ const errorMsgs: string[] = [];
+ if (notFound.length > 0) errorMsgs.push(`Core DAppNode Packages ${notFound.join(", ")} are not found`);
+ if (notRunning.length > 0) errorMsgs.push(`Core DAppNode Packages ${notRunning.join(", ")} are not running`);
+ const ok = notFound.length === 0 && notRunning.length === 0;
+ return {
+ ok,
+ msg: ok ? "All core DAppNode Packages are running" : errorMsgs.join(". "),
+ solutions: [
+ "Make sure the disk is not too full. If so DAppNode automatically stops the IPFS package to prevent it from becoming un-usable",
+ "Go to the System tab and restart each stopped DAppNode Package. Please inspect the logs to understand cause and report it if it was not expected"
+ ]
+ };
+}
diff --git a/packages/admin-ui/src/pages-new/utils/discourseTopic.ts b/packages/admin-ui/src/pages-new/utils/discourseTopic.ts
new file mode 100644
index 0000000000..08f0b5c79e
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/utils/discourseTopic.ts
@@ -0,0 +1,53 @@
+import { PackageVersionData, HostDiagnoseItem } from "@dappnode/types";
+import { topicBaseUrl } from "params";
+
+interface TopicBodySection {
+ title: string;
+ items: { name: string; data: string }[];
+}
+
+export function formatTopicBody(
+ coreDnpVersions: { name: string; version: string | PackageVersionData }[],
+ hostDiagnoseItems: HostDiagnoseItem[]
+): string {
+ const sections: TopicBodySection[] = [
+ {
+ title: "Core DAppNode Packages versions",
+ items: coreDnpVersions
+ .map(({ name, version }) => ({
+ name,
+ data: typeof version === "object" ? printVersionData(version) : version
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name))
+ },
+ {
+ title: "System info",
+ items: hostDiagnoseItems
+ }
+ ];
+
+ return [
+ "*Before filing a new topic, please **provide the following information**.*",
+ ...sections
+ .filter(({ items }) => items.length)
+ .map(({ title, items }) => `## ${title}\n` + items.map(({ name, data }) => `- **${name}**: ${data}`).join("\n"))
+ ].join("\n\n");
+}
+
+export function formatTopicUrl(body: string) {
+ const topicCategory = "5";
+ const title = "";
+ const params = [
+ "title=" + encodeURIComponent(title),
+ "body=" + encodeURIComponent(body),
+ "category_id=" + encodeURIComponent(topicCategory)
+ ];
+ return topicBaseUrl + "?" + params.join("&");
+}
+
+function printVersionData(versionData: PackageVersionData): string {
+ const { branch, commit, version } = versionData || {};
+ return [version, branch && branch !== "master" && `branch: ${branch}`, commit && `commit: ${commit.slice(0, 8)}`]
+ .filter((data) => data)
+ .join(", ");
+}
diff --git a/packages/admin-ui/src/pages-new/utils/routeData.ts b/packages/admin-ui/src/pages-new/utils/routeData.ts
new file mode 100644
index 0000000000..52e1caf48d
--- /dev/null
+++ b/packages/admin-ui/src/pages-new/utils/routeData.ts
@@ -0,0 +1,9 @@
+import { withLegacyBase } from "utils/path";
+
+/**
+ * Get the installer path for a given DAppNode package name.
+ */
+export const getInstallerPath = (dnpName: string) => {
+ if (dnpName.includes("public")) return withLegacyBase("installer/public");
+ return withLegacyBase("installer/dnp");
+};
diff --git a/packages/admin-ui/src/pages/community/data.ts b/packages/admin-ui/src/pages/community/data.ts
index d8395eb117..eebec87c52 100644
--- a/packages/admin-ui/src/pages/community/data.ts
+++ b/packages/admin-ui/src/pages/community/data.ts
@@ -15,7 +15,8 @@ export const communityTypes: CommunityItem[] = [
title: "Discord",
icon: FaDiscord,
actions: [DiscordActions],
- text: "DAppNode has a vibrant community. You can get support, share your experience, and just hang out with other Node Runners in our Discord Server"
+ text:
+ "DAppNode has a vibrant community. You can get support, share your experience, and just hang out with other Node Runners in our Discord Server"
},
{
subPath: "gitcoin",
@@ -29,7 +30,8 @@ export const communityTypes: CommunityItem[] = [
title: "Discourse",
icon: FaDiscourse,
actions: [DiscourseActions],
- text: "How-tos, Deep Dives, support questions… our Forum is the place where information that shouldn’t be lost in a chat should go!"
+ text:
+ "How-tos, Deep Dives, support questions… our Forum is the place where information that shouldn’t be lost in a chat should go!"
},
{
subPath: "github",
diff --git a/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx b/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx
index c3c6a31c75..beb92ae567 100644
--- a/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx
+++ b/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx
@@ -29,6 +29,7 @@ import { diff } from "semver";
import Button from "components/Button";
import { pathName as systemPathName, subPaths as systemSubPaths } from "pages/system/data";
import { Notifications } from "./Steps/Notifications";
+import { withLegacyBase } from "utils/path";
interface InstallDnpViewProps {
dnp: RequestedDnp;
@@ -158,7 +159,7 @@ const InstallDnpView: React.FC = ({ dnp, progressLogs }) =>
setTimeout(() => {
if (componentIsMounted.current) {
setShowSuccess(false);
- navigate("/" + packagesRelativePath + "/" + dnpName + "/info");
+ navigate(withLegacyBase(`${packagesRelativePath}/${dnpName}/info`));
}
}, 1000);
}
@@ -375,7 +376,7 @@ const InstallDnpView: React.FC = ({ dnp, progressLogs }) =>
continuing the installation. You can do it with our update Docker button in{" "}
System / Advanced tab.
-
+
{"Navigate there"}
diff --git a/packages/admin-ui/src/pages/installer/components/InstallerNavigator.tsx b/packages/admin-ui/src/pages/installer/components/InstallerNavigator.tsx
index 21d3fa281a..a5b4e0fbf4 100644
--- a/packages/admin-ui/src/pages/installer/components/InstallerNavigator.tsx
+++ b/packages/admin-ui/src/pages/installer/components/InstallerNavigator.tsx
@@ -9,6 +9,7 @@ import { confirm } from "components/ConfirmDialog";
import { subPaths } from "../data";
import "./installer.scss";
import { RouteType } from "types";
+import { withLegacyBase } from "utils/path";
type NavRoute = {
name: string;
@@ -46,7 +47,7 @@ Nobody, DAppNode Association, DAppNodeDAO or anyone, will be held responsible fo
]
})
);
- navigate("/installer/public");
+ navigate(withLegacyBase("installer/public"));
} catch {
// user cancelled
}
diff --git a/packages/admin-ui/src/pages/installer/components/dappnodeDappstore/InstallerDnp.tsx b/packages/admin-ui/src/pages/installer/components/dappnodeDappstore/InstallerDnp.tsx
index 7cd655620b..b2c2a468b3 100644
--- a/packages/admin-ui/src/pages/installer/components/dappnodeDappstore/InstallerDnp.tsx
+++ b/packages/admin-ui/src/pages/installer/components/dappnodeDappstore/InstallerDnp.tsx
@@ -22,6 +22,8 @@ import { fetchDnpDirectory } from "services/dnpDirectory/actions";
import { confirmPromise } from "components/ConfirmDialog";
import { stakehouseLsdUrl } from "params";
import { InstallerAIBanner } from "../aiDappstore/InstallerAiBanner";
+import { relativePath as stakersRelativePath } from "pages/stakers/data";
+import { withLegacyBase } from "utils/path";
export const InstallerDnp: React.FC = () => {
const navigate = useNavigate();
@@ -58,8 +60,8 @@ export const InstallerDnp: React.FC = () => {
// - Mainnet: http://my.dappnode/stakers/mainnet
// - Gnosis: http://my.dappnode/stakers/gnosis
// - Stakehouse: http://my.dappnode/stakers/stakehouse
- if (id === "ethereum.dnp.dappnode.eth") navigate("/stakers/ethereum");
- else if (id === "gnosis.dnp.dappnode.eth") navigate("/stakers/gnosis");
+ if (id === "ethereum.dnp.dappnode.eth") navigate(withLegacyBase(stakersRelativePath));
+ else if (id === "gnosis.dnp.dappnode.eth") navigate(withLegacyBase("stakers/gnosis"));
else if (id === "stakehouse.dnp.dappnode.eth") {
// open a dialog that says it will open an external link, are you sure?
confirmPromise({
diff --git a/packages/admin-ui/src/pages/installer/data.ts b/packages/admin-ui/src/pages/installer/data.ts
index dce48f3769..1a1ebb5992 100644
--- a/packages/admin-ui/src/pages/installer/data.ts
+++ b/packages/admin-ui/src/pages/installer/data.ts
@@ -1,8 +1,9 @@
// This will be used later in our root reducer and selectors
export const relativePath = "installer/dnp";
+import { withLegacyBase } from "utils/path";
export const getInstallerPath = (dnpName: string) => {
- if (dnpName.includes("public")) return "/installer/public";
- return "/installer/dnp";
+ if (dnpName.includes("public")) return withLegacyBase("installer/public");
+ return withLegacyBase("installer/dnp");
};
export const rootPath = "installer/*";
export const title = "DAppStore";
diff --git a/packages/admin-ui/src/pages/notifications/data.ts b/packages/admin-ui/src/pages/notifications/data.ts
index fb06e1aa52..cbac2963b0 100644
--- a/packages/admin-ui/src/pages/notifications/data.ts
+++ b/packages/admin-ui/src/pages/notifications/data.ts
@@ -1,4 +1,4 @@
-export const relativePath = "notifications/inbox"; // default redirect to inbox
+export const relativePath = "notifications/inbox"; // default redirect to inbox
export const rootPath = "notifications/*";
export const title = "Notifications";
export const pathName = "notifications";
@@ -10,5 +10,5 @@ export const subPaths = {
inbox: "inbox",
settings: "settings",
legacy: "legacy",
- devices : "devices",
+ devices: "devices"
};
diff --git a/packages/admin-ui/src/pages/notifications/tabs/Legacy/index.tsx b/packages/admin-ui/src/pages/notifications/tabs/Legacy/index.tsx
index b3cec5ebc3..68b4cc9172 100644
--- a/packages/admin-ui/src/pages/notifications/tabs/Legacy/index.tsx
+++ b/packages/admin-ui/src/pages/notifications/tabs/Legacy/index.tsx
@@ -21,10 +21,7 @@ export function LegacyNotifications() {
🔁 To enable them, make sure you check out the new{" "}
-
- Settings Notifications tab
- {" "}
-
+ Settings Notifications tab
📘 For full details about the new system, see our{" "}
Notifications Documentation
diff --git a/packages/admin-ui/src/pages/packages/components/Info/RemovePackage.tsx b/packages/admin-ui/src/pages/packages/components/Info/RemovePackage.tsx
index b8ad3cf106..0960804ce0 100644
--- a/packages/admin-ui/src/pages/packages/components/Info/RemovePackage.tsx
+++ b/packages/admin-ui/src/pages/packages/components/Info/RemovePackage.tsx
@@ -11,6 +11,7 @@ import { prettyDnpName } from "utils/format";
import { InstalledPackageDetailData } from "@dappnode/types";
import { relativePath as packagesRelativePath } from "../../data";
import { markdownList } from "utils/markdown";
+import { withLegacyBase } from "utils/path";
import "./removePackage.scss";
interface WarningItem {
@@ -87,7 +88,7 @@ export function RemovePackage({ dnp }: { dnp: InstalledPackageDetailData }) {
message: `Removing ${name} ${deleteVolumes ? " and volumes" : ""}...`,
onSuccess: `Removed ${name}`
});
- navigate("/" + packagesRelativePath);
+ navigate(withLegacyBase(packagesRelativePath));
} catch (e) {
console.error(e);
}
diff --git a/packages/admin-ui/src/pages/packages/components/PackagesNavigator.tsx b/packages/admin-ui/src/pages/packages/components/PackagesNavigator.tsx
index 7c05eef07c..a06a19fb44 100644
--- a/packages/admin-ui/src/pages/packages/components/PackagesNavigator.tsx
+++ b/packages/admin-ui/src/pages/packages/components/PackagesNavigator.tsx
@@ -3,10 +3,11 @@ import React from "react";
import { Routes, Route, NavLink, useMatch } from "react-router-dom";
import { SectionNavigator } from "components/SectionNavigator";
import { PackagesList } from "../components/PackagesList";
-import { subPaths, title } from "../data";
+import { basePath, subPaths, title } from "../data";
import { PackageById } from "../pages/ById";
import { RouteType } from "types";
import Title from "components/Title";
+import { withLegacyBase } from "utils/path";
import "./packages.scss";
type NavRoute = {
@@ -45,7 +46,7 @@ export const PackagesNavigator: React.FC = () => {
];
// Hide navbar when in a package detail view
- const scopePath: string = "/packages/:scope";
+ const scopePath: string = `${withLegacyBase(basePath)}/:scope`;
const match = useMatch({ path: scopePath, end: true });
const isBaseSubpath = !!match && routesForNavbar.some((r) => r.link === (match.params?.scope ?? ""));
diff --git a/packages/admin-ui/src/pages/packages/components/packages.scss b/packages/admin-ui/src/pages/packages/components/packages.scss
index eb8bb8add2..de1fdf33b7 100644
--- a/packages/admin-ui/src/pages/packages/components/packages.scss
+++ b/packages/admin-ui/src/pages/packages/components/packages.scss
@@ -67,7 +67,7 @@
/* Customize name inside NavLink */
a:hover {
- color: var(--dappnode-color);
+ color: var(--dappnode-color) !important;
text-decoration: none;
transition: color 0.2s ease;
}
diff --git a/packages/admin-ui/src/pages/packages/pages/ById.tsx b/packages/admin-ui/src/pages/packages/pages/ById.tsx
index fdc5f26616..c9d5b7b8e6 100644
--- a/packages/admin-ui/src/pages/packages/pages/ById.tsx
+++ b/packages/admin-ui/src/pages/packages/pages/ById.tsx
@@ -19,6 +19,7 @@ import Title from "components/Title";
import { prettyDnpName } from "utils/format";
import { AlertPackageUpdateAvailable } from "../components/AlertPackageUpdateAvailable";
import { MdKeyboardArrowLeft } from "react-icons/md";
+import { withLegacyBase } from "@/utils/path";
import "../components/packages.scss";
export const PackageById: React.FC = () => {
@@ -109,10 +110,12 @@ export const PackageById: React.FC = () => {
available: true
}
].filter((route) => route.available);
-
return (
-
+
{(isCore ? `${systemSubPath}` : `${mySubPath}`).toUpperCase()} PACKAGES
diff --git a/packages/admin-ui/src/pages/premium/components/backupNode/BackupNode.tsx b/packages/admin-ui/src/pages/premium/components/backupNode/BackupNode.tsx
index 4e5dc0aefc..8368761634 100644
--- a/packages/admin-ui/src/pages/premium/components/backupNode/BackupNode.tsx
+++ b/packages/admin-ui/src/pages/premium/components/backupNode/BackupNode.tsx
@@ -8,6 +8,7 @@ import { Card } from "react-bootstrap";
import { NetworkBackup } from "./NetworkBackup";
import { availableNetworks, useBackupNodeData } from "hooks/premium/useBackupNodeData";
import { Network } from "@dappnode/types";
+import { withLegacyBase } from "utils/path";
import "./backupNode.scss";
@@ -22,8 +23,8 @@ export function BackupNode({ isActivated: isPremium, hashedLicense }: { isActiva
// if no backup subroute selected, redirect automatically to first network
useEffect(() => {
if (
- window.location.pathname === `/${basePath}/${subPaths.backupNode}` ||
- window.location.pathname === `/${basePath}/${subPaths.backupNode}/`
+ window.location.pathname === withLegacyBase(`${basePath}/${subPaths.backupNode}`) ||
+ window.location.pathname === `${withLegacyBase(`${basePath}/${subPaths.backupNode}`)}/`
)
navigate(`ethereum`);
}, []);
diff --git a/packages/admin-ui/src/pages/sdk/components/SdkHome.tsx b/packages/admin-ui/src/pages/sdk/components/SdkHome.tsx
index 83070f6c3d..6516fb3cdc 100644
--- a/packages/admin-ui/src/pages/sdk/components/SdkHome.tsx
+++ b/packages/admin-ui/src/pages/sdk/components/SdkHome.tsx
@@ -1,7 +1,7 @@
import React from "react";
import { title } from "../data";
import newTabProps from "utils/newTabProps";
-import { sdkPublishAppUrl, sdkGuideUrl } from "params";
+import { sdkPublishAppUrl, sdkRepoUrl } from "params";
// Components
import Title from "components/Title";
import Card from "components/Card";
@@ -42,7 +42,7 @@ export default function SdkHome() {
docker image and publish it on the Aragon Package Manager (APM) on the Ethereum mainnet
-
+
Full Guide
diff --git a/packages/admin-ui/src/pages/stakers/components/StakerNetwork.tsx b/packages/admin-ui/src/pages/stakers/components/StakerNetwork.tsx
index 31d66d8476..407635c699 100644
--- a/packages/admin-ui/src/pages/stakers/components/StakerNetwork.tsx
+++ b/packages/admin-ui/src/pages/stakers/components/StakerNetwork.tsx
@@ -176,7 +176,7 @@ export default function StakerNetwork({ network, description }: { network: Netwo
Sepolia network is not intended for staking , as it only supports whitelisted validators. Running a
Sepolia node is still useful for L2s, infrastructure testing, and other use cases. To test staking, run a{" "}
- Hoodi node
+ Hoodi node
{" "}
instead.
diff --git a/packages/admin-ui/src/params.ts b/packages/admin-ui/src/params.ts
index 0aa2a93203..ab5a09ac40 100755
--- a/packages/admin-ui/src/params.ts
+++ b/packages/admin-ui/src/params.ts
@@ -1,4 +1,5 @@
import { urlJoin } from "utils/url";
+import { withLegacyBase } from "utils/path";
// JSON RPC API
export const apiUrl = "/";
@@ -74,11 +75,11 @@ export const dappnodeForumUrl = "https://forum.dappnode.io";
export const topicBaseUrl = `https://forum.dappnode.io/new-topic`;
export const discordInviteUrl = "https://discord.gg/dappnode";
// zkEvm
-export const packageInfoPath = `/packages/my/${zkevmDnpName}/info`;
+export const packageInfoPath = withLegacyBase(`packages/my/${zkevmDnpName}/info`);
export const zkevmUiUrl = "http://ui.zkevm-tokens-withdrawal.dappnode/";
export const sdkPublishAppUrl = "https://dappnode.github.io/sdk-publish/";
-export const sdkGuideUrl = "https://github.com/dappnode/DAppNodeSDK";
+export const sdkRepoUrl = "https://github.com/dappnode/DAppNodeSDK";
export const githubNewIssueDappnodeUrl = "https://github.com/dappnode/DAppNode/issues/new";
export const surveyUrl = "https://goo.gl/forms/DSy1J1OlQGpdyhD22";
@@ -93,6 +94,8 @@ const docsBaseUrl = "https://docs.dappnode.io";
export const docsUrl = {
main: docsBaseUrl,
+ userDocumentation: `${docsBaseUrl}/docs/user/getting-started/choose-your-path`,
+ devsDocumentation: `${docsBaseUrl}/docs/dev`,
recoverPasswordGuide: `${docsBaseUrl}/docs/user/getting-started/register#troubleshooting`,
connectToRouter: `${docsBaseUrl}/docs/user/getting-started/connect-dappnode-to-the-router`,
connectWifi: `${docsBaseUrl}/docs/user/access-your-dappnode/wifi`,
@@ -114,7 +117,10 @@ export const docsUrl = {
premiumBackupNode: `${docsBaseUrl}/docs/user/dappnode-premium/premium-services#backup-node-for-validators`,
premiumBackupValidatorsLimit: `${docsBaseUrl}/docs/user/dappnode-premium/premium-services#validators-limit`,
starknetDocs: `${docsBaseUrl}/docs/user/staking/starknet/solo`,
- uiTelemetry: `${docsBaseUrl}/docs/user/ui-telemetry`
+ uiTelemetry: `${docsBaseUrl}/docs/user/ui-telemetry`,
+ publishPkgGuide: `${docsBaseUrl}/docs/dev/package-publishing/publish-packages-clients`,
+ ownershipPkgGuide: `${docsBaseUrl}/docs/dev/package-publishing/package-ownership`,
+ sdkGuideUrl: `${docsBaseUrl}/docs/dev/sdk/overview`
};
export const forumUrl = {
@@ -134,6 +140,7 @@ export const troubleShootMountpointsGuideUrl = "https://docs.dappnode.io/develop
export const dappnodeUserGuideUrl = "https://docs.dappnode.io/user/faq/general";
export const explorerGitcoinUrl =
"https://explorer.gitcoin.co/#/round/1/0xdf22a2c8f6ba9376ff17ee13e6154b784ee92094/0xdf22a2c8f6ba9376ff17ee13e6154b784ee92094-17";
+export const givethDappnodeDonationsUrl = "https://giveth.io/project/dappnode";
export const dappnodeGithub = "https://github.com/dappnode/DAppNode";
export const dappnodeDiscourse = "https://forum.dappnode.io/";
export const dappnodeDiscord = "https://discord.gg/dappnode";
diff --git a/packages/admin-ui/src/styles/bootstrap-scoped.scss b/packages/admin-ui/src/styles/bootstrap-scoped.scss
new file mode 100644
index 0000000000..371ba38b5c
--- /dev/null
+++ b/packages/admin-ui/src/styles/bootstrap-scoped.scss
@@ -0,0 +1,107 @@
+/**
+ * Scoped Bootstrap styles.
+ *
+ * Bootstrap styles (reboot + class-based components) are nested under
+ * `.legacy-bootstrap` so they only apply to the legacy /staking/* routes
+ * wrapped in
.
+ *
+ * New routes (/, /ai, etc.) are NOT affected by Bootstrap styles.
+ *
+ * Strategy:
+ * 1. Import Bootstrap's configuration at root level (no CSS output).
+ * 2. Import _root.scss at root level for --bs-* CSS custom properties.
+ * 3. Nest _reboot.scss + all class-based partials under .legacy-bootstrap.
+ * 4. Apply body-level typography on .legacy-bootstrap itself (since the
+ * scoped `body` rule won't match the real ).
+ * 5. Import custom SCSS overrides inside .legacy-bootstrap so they share
+ * the same specificity as the Bootstrap component selectors.
+ * 6. Import portal-based components (modal, tooltip, popover, offcanvas)
+ * at root level — they render on via React portals.
+ * 7. Import structural/theme SCSS at root level (uses :root/body/#id).
+ */
+
+// 1. Configuration — no CSS output, needed by the partials below
+@import "bootstrap/scss/functions";
+@import "bootstrap/scss/variables";
+@import "bootstrap/scss/variables-dark";
+@import "bootstrap/scss/maps";
+@import "bootstrap/scss/mixins";
+@import "bootstrap/scss/utilities";
+
+// 2. CSS custom properties on :root
+@import "bootstrap/scss/root";
+
+// DAppNode color variables on :root (extracted from dappnode_colors.scss).
+@import "./dappnode-color-vars";
+
+// 3. Reboot is imported inside .legacy-bootstrap (below) so its element
+// resets (a, *, body, button, etc.) don't leak to new routes.
+
+// 4. Class-based component styles — scoped under .legacy-bootstrap
+.legacy-bootstrap {
+ // Reboot (element-level normalisation) — scoped so it only affects
+ // legacy routes. Body-level styles are re-applied on & below.
+ @import "bootstrap/scss/reboot";
+
+ // Apply body-level styles to the wrapper itself so typography,
+ // colors, and font-family are inherited by descendants.
+ margin: 0;
+ font-family: var(--#{$prefix}body-font-family);
+ @include font-size(var(--#{$prefix}body-font-size));
+ font-weight: var(--#{$prefix}body-font-weight);
+ line-height: var(--#{$prefix}body-line-height);
+ color: var(--#{$prefix}body-color);
+ -webkit-text-size-adjust: 100%;
+ -webkit-tap-highlight-color: rgba($black, 0);
+
+ // ── Layout & components ─────────────────────────────────────────
+ @import "bootstrap/scss/type";
+ @import "bootstrap/scss/images";
+ @import "bootstrap/scss/containers";
+ @import "bootstrap/scss/grid";
+ @import "bootstrap/scss/tables";
+ @import "bootstrap/scss/forms";
+ @import "bootstrap/scss/buttons";
+ @import "bootstrap/scss/transitions";
+ @import "bootstrap/scss/dropdown";
+ @import "bootstrap/scss/button-group";
+ @import "bootstrap/scss/nav";
+ @import "bootstrap/scss/navbar";
+ @import "bootstrap/scss/card";
+ @import "bootstrap/scss/accordion";
+ @import "bootstrap/scss/breadcrumb";
+ @import "bootstrap/scss/pagination";
+ @import "bootstrap/scss/badge";
+ @import "bootstrap/scss/alert";
+ @import "bootstrap/scss/progress";
+ @import "bootstrap/scss/list-group";
+ @import "bootstrap/scss/close";
+ @import "bootstrap/scss/toasts";
+ @import "bootstrap/scss/carousel";
+ @import "bootstrap/scss/spinners";
+ @import "bootstrap/scss/placeholders";
+
+ // Helpers
+ @import "bootstrap/scss/helpers";
+
+ // Utilities
+ @import "bootstrap/scss/utilities/api";
+
+ // 5. Custom overrides — INSIDE the scope so they have the same
+ // .legacy-bootstrap ancestor specificity as the Bootstrap
+ // component selectors they need to override.
+ @import "../dappnode_styles";
+ @import "../dappnode_colors";
+}
+
+// Portal-based components — rendered outside .legacy-bootstrap (appended to
+// by react-bootstrap). Must be at root level so selectors match.
+@import "bootstrap/scss/modal";
+@import "bootstrap/scss/tooltip";
+@import "bootstrap/scss/popover";
+@import "bootstrap/scss/offcanvas";
+
+// 6. Structural / theme styles — stay at root level because they target
+// :root, body, html, #sidebar, #main, #topbar (outside .legacy-bootstrap).
+@import "../light_dark";
+@import "../layout";
diff --git a/packages/admin-ui/src/styles/dappnode-color-vars.scss b/packages/admin-ui/src/styles/dappnode-color-vars.scss
new file mode 100644
index 0000000000..79868fb1fd
--- /dev/null
+++ b/packages/admin-ui/src/styles/dappnode-color-vars.scss
@@ -0,0 +1,57 @@
+/**
+ * DAppNode color variables — extracted from dappnode_colors.scss.
+ *
+ * These CSS custom properties are used throughout the legacy UI and must
+ * live on :root (not scoped under .legacy-bootstrap) so they cascade
+ * to all descendants. The class-based overrides (btn-dappnode, badges,
+ * toast colors, etc.) remain in dappnode_colors.scss and are imported
+ * inside the .legacy-bootstrap scope for correct specificity.
+ */
+:root {
+ --dappnode-white-color: #fff;
+ --dappnode-strong-main-color: #00b1f4;
+ --dappnode-darker-main-color: #007dfc;
+ --dappnode-shadow-main-color: #06d4e7;
+ --dappnode-light-main-color: #a0bdbb;
+ --dappnode-gray-main-color: #748888;
+ --dappnode-links-color: #00b1f4;
+ --dappnode-links-darker-color: #007dfc;
+ --dappnode-complimentary-color: #bc2f39;
+ // Border colors
+ --border-color: #e5e5e5;
+ --color-light-border: #e5e5e5;
+ --color-dark-border: #616161;
+ --border-style-light: var(--border-size) solid var(--color-light-border);
+ --border-style-dark: var(--border-size) solid var(--color-dark-border);
+
+ // LIGHT mode
+ --color-light-background-sidebar-topbar: white;
+ --color-light-background-main: #f7f9f9;
+
+ // DARK mode
+ --color-dark-background-sidebar: #212121;
+ --color-dark-background-topbar: #313131;
+ --color-dark-background-main: #313131;
+ --color-dark-maintext: #ffffff;
+ --color-dark-secondarytext: #dddddd;
+ --color-dark-tertiarytext: #cccccc;
+ --color-dark-quaternarytext: #c1c1c1;
+ --color-dark-card: #414141;
+ --color-dark-card-hover: #6a6a6a;
+ --color-dark-danger-background: #8d2222;
+
+ // Buttons
+ --dappnode-white-color: #fff;
+
+ // Alert colors
+ --danger-color: var(--dappnode-complimentary-color);
+ --warning-color: #ffcc00;
+ --success-color: var(--dappnode-strong-main-color);
+ --success-green-color: #34a853;
+ /* Shortcuts */
+ --dappnode-color: var(--dappnode-strong-main-color);
+ /* Text colors */
+ --light-text-color: #757575;
+ /* Opacity shades */
+ --opacity-soft: 0.6;
+}
diff --git a/packages/admin-ui/src/styles/tailwind.css b/packages/admin-ui/src/styles/tailwind.css
new file mode 100644
index 0000000000..067619bace
--- /dev/null
+++ b/packages/admin-ui/src/styles/tailwind.css
@@ -0,0 +1,364 @@
+@layer theme, base, components, utilities;
+
+/* Tailwind theme with prefix */
+@import "tailwindcss/theme.css" layer(theme) prefix(tw);
+
+/* Keep preflight disabled for now so Bootstrap/legacy styles are not reset */
+
+/* Animation utilities for prefixed Tailwind setups */
+@import "tw-animate-css/prefix";
+
+/* Tailwind utilities only, still prefixed */
+@import "tailwindcss/utilities.css" layer(utilities) prefix(tw) source(none);
+
+/* Scan app files for tw: classes */
+@source "../**/*.tsx";
+
+/* Custom utilities (available as tw:no-scrollbar etc.) */
+@utility no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+@custom-variant dark (&:is(.dark *));
+
+/*
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ * ⚠️ MIGRATION NOTE — TEMPORARY CODE
+ * Scoped base reset for new Tailwind pages
+ *
+ * Since Tailwind preflight is disabled (to keep Bootstrap working on
+ * legacy routes), we manually apply the critical resets ONLY inside
+ * elements that opt-in with the `.tw-base` class.
+ *
+ * This gives new pages proper box-sizing, font-family, line-height,
+ * and removes the fixed `height:100%` constraint from the legacy
+ * layout.scss that breaks `min-h-svh` on the sidebar.
+ *
+ * This entire `.tw-base` block (including the scoped reset rules
+ * below) can be DELETED once the Bootstrap → Tailwind migration is
+ * complete. At that point:
+ * 1. Remove Bootstrap and its scoped import (bootstrap-scoped.scss).
+ * 2. Enable Tailwind preflight by adding:
+ * @import "tailwindcss/preflight.css" layer(base);
+ * after the theme import at the top of this file.
+ * 3. Delete every `.tw-base` rule in the @layer base {} block.
+ * 4. Remove the `.tw-base` wrapper class from layouts and
+ * components (NewPageLayout, AiLayout, sidebar SheetContent…).
+ * Preflight will then apply these same resets globally, making the
+ * scoped approach unnecessary.
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ */
+@layer base {
+ .tw-base {
+ /* Apply the same resets Tailwind preflight would, scoped */
+ box-sizing: border-box;
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
+ Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+ tab-size: 4;
+ }
+
+ /*
+ * Page-level layout overrides — only for the top-level .tw-base wrapper
+ * used by layouts (AiLayout, NewPageLayout), NOT for portal elements.
+ * The `:not([data-slot])` selector excludes Radix portal content which
+ * always carries a data-slot attribute.
+ */
+ .tw-base:not([data-slot]) {
+ height: auto !important;
+ min-height: 100svh;
+ }
+
+ /*
+ * Scoped preflight — applies to everything inside .tw-base.
+ *
+ * The mobile sidebar (rendered via a React portal on ) also
+ * gets these resets because the sidebar component adds the .tw-base
+ * class to its SheetContent wrapper.
+ */
+ .tw-base *,
+ .tw-base *::before,
+ .tw-base *::after {
+ box-sizing: border-box;
+ border-width: 0;
+ border-style: solid;
+ border-color: var(--border);
+ }
+
+ /* Reset common elements */
+ .tw-base h1,
+ .tw-base h2,
+ .tw-base h3,
+ .tw-base h4,
+ .tw-base h5,
+ .tw-base h6,
+ .tw-base p {
+ margin: 0;
+ font-weight: inherit;
+ font-size: inherit;
+ }
+
+ .tw-base ul,
+ .tw-base ol {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ .tw-base a {
+ color: inherit;
+ text-decoration: inherit;
+ }
+
+ .tw-base button,
+ .tw-base input,
+ .tw-base optgroup,
+ .tw-base select,
+ .tw-base textarea {
+ font-family: inherit;
+ font-feature-settings: inherit;
+ font-variation-settings: inherit;
+ font-size: 100%;
+ font-weight: inherit;
+ line-height: inherit;
+ letter-spacing: inherit;
+ color: var(--foreground);
+ margin: 0;
+ padding: 0;
+ }
+
+ .tw-base button,
+ .tw-base [role="button"] {
+ cursor: pointer;
+ }
+
+ .tw-base img,
+ .tw-base svg,
+ .tw-base video {
+ display: block;
+ max-width: 100%;
+ }
+
+ .tw-base img,
+ .tw-base video {
+ height: auto;
+ }
+
+ /* Scrollbar-less containers (used by sidebar) */
+ .tw-base .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+ .tw-base .no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+
+ /*
+ * Sonner toaster reset — Sonner renders a into ,
+ * outside .tw-base. Give it the same critical resets.
+ */
+ [data-sonner-toaster],
+ [data-sonner-toaster] *,
+ [data-sonner-toaster] *::before,
+ [data-sonner-toaster] *::after {
+ box-sizing: border-box;
+ border-width: 0;
+ border-style: solid;
+ border-color: var(--border);
+ }
+
+ [data-sonner-toaster] {
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
+ Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+}
+
+/* shadcn tokens + Dappnode design tokens */
+@theme inline {
+ /* ── Semantic color aliases (shadcn) ──────────────────────────── */
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-success: var(--success);
+ --color-success-foreground: var(--success-foreground);
+ --color-caution: var(--caution);
+ --color-caution-foreground: var(--caution-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+
+ /* ── Dappnode brand palette ───────────────────────────────────── */
+ --color-dn-blue: var(--dn-blue);
+ --color-dn-cyan: var(--dn-cyan);
+ --color-dn-orange: var(--dn-orange);
+ --color-dn-pink: var(--dn-pink);
+ --color-dn-purple: var(--dn-purple);
+
+ /* ── Sidebar tokens ───────────────────────────────────────────── */
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+
+ /* ── Radius ────────────────────────────────────────────────────── */
+ --radius-sm: calc(var(--radius) * 0.6);
+ --radius-md: calc(var(--radius) * 0.8);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) * 1.4);
+ --radius-2xl: calc(var(--radius) * 2);
+
+ /* ── Spacing primitives ───────────────────────────────────────── */
+ --spacing-page-x: var(--page-padding-x);
+ --spacing-page-y: var(--page-padding-y);
+ --spacing-section: var(--section-gap);
+ --spacing-card: var(--card-gap);
+ --spacing-header-gap: var(--header-gap);
+ --spacing-step: var(--step-gap);
+ --spacing-topbar-h: var(--topbar-height);
+ --spacing-content-max: var(--content-max-w);
+}
+
+/*
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ * Dappnode branded palette
+ *
+ * Blue #00B1F4 → oklch(0.72 0.15 230)
+ * Cyan #06D4E7 → oklch(0.80 0.13 200)
+ * Orange #FC9E22 → oklch(0.78 0.17 70)
+ * Pink #E60AF6 → oklch(0.58 0.30 320)
+ * Purple #5231C6 → oklch(0.40 0.24 290)
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ */
+
+:root {
+ /* ── Brand primitives (constant across light/dark) ────────────── */
+ --dn-blue: oklch(0.72 0.15 230);
+ --dn-cyan: oklch(0.8 0.13 200);
+ --dn-orange: oklch(0.78 0.17 70);
+ --dn-pink: oklch(0.58 0.3 320);
+ --dn-purple: oklch(0.4 0.24 290);
+
+ /* ── Layout primitives ────────────────────────────────────────── */
+ --page-padding-x: 1.5rem;
+ --page-padding-y: 2rem;
+ --section-gap: 2rem;
+ --card-gap: 1rem;
+ --header-gap: 0.5rem;
+ --step-gap: 1.5rem;
+ --topbar-height: 3rem;
+ --content-max-w: 64rem;
+ --radius: 0.625rem;
+
+ /* ── Semantic tokens (light) ──────────────────────────────────── */
+ --background: oklch(0.98 0.005 220);
+ --foreground: oklch(0.15 0.01 240);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.15 0.01 240);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.15 0.01 240);
+ --primary: var(--dn-blue);
+ --primary-foreground: oklch(0.99 0 0);
+ --secondary: oklch(0.95 0.02 220);
+ --secondary-foreground: oklch(0.2 0.02 240);
+ --muted: oklch(0.95 0.01 220);
+ --muted-foreground: oklch(0.5 0.02 240);
+ --accent: var(--dn-cyan);
+ --accent-foreground: oklch(0.99 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --destructive-foreground: oklch(0.985 0 0);
+ --success: oklch(0.6 0.19 145);
+ --success-foreground: oklch(0.985 0 0);
+ --caution: oklch(0.75 0.18 65);
+ --caution-foreground: oklch(0.25 0.05 65);
+ --border: oklch(0.91 0.01 220);
+ --input: oklch(0.91 0.01 220);
+ --ring: var(--dn-blue);
+ --chart-1: var(--dn-blue);
+ --chart-2: var(--dn-cyan);
+ --chart-3: var(--dn-orange);
+ --chart-4: var(--dn-pink);
+ --chart-5: var(--dn-purple);
+
+ /* ── Sidebar (light) ──────────────────────────────────────────── */
+ --sidebar: oklch(0.98 0.005 220);
+ --sidebar-foreground: oklch(0.15 0.01 240);
+ --sidebar-primary: var(--dn-blue);
+ --sidebar-primary-foreground: oklch(0.99 0 0);
+ --sidebar-accent: oklch(0.93 0.02 220);
+ --sidebar-accent-foreground: oklch(0.15 0.01 240);
+ --sidebar-border: oklch(0.91 0.01 220);
+ --sidebar-ring: var(--dn-blue);
+}
+
+.dark {
+ --background: oklch(0.14 0.01 240);
+ --foreground: oklch(0.96 0.005 220);
+ --card: oklch(0.19 0.015 240);
+ --card-foreground: oklch(0.96 0.005 220);
+ --popover: oklch(0.19 0.015 240);
+ --popover-foreground: oklch(0.96 0.005 220);
+ --primary: var(--dn-blue);
+ --primary-foreground: oklch(0.12 0.02 240);
+ --secondary: oklch(0.24 0.02 240);
+ --secondary-foreground: oklch(0.96 0.005 220);
+ --muted: oklch(0.24 0.015 240);
+ --muted-foreground: oklch(0.65 0.02 220);
+ --accent: var(--dn-cyan);
+ --accent-foreground: oklch(0.99 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --destructive-foreground: oklch(0.985 0 0);
+ --success: oklch(0.65 0.2 145);
+ --success-foreground: oklch(0.985 0 0);
+ --caution: oklch(0.8 0.16 65);
+ --caution-foreground: oklch(0.15 0.05 65);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: var(--dn-blue);
+ --chart-1: var(--dn-blue);
+ --chart-2: var(--dn-cyan);
+ --chart-3: var(--dn-orange);
+ --chart-4: var(--dn-pink);
+ --chart-5: var(--dn-purple);
+
+ /* ── Sidebar (dark) ───────────────────────────────────────────── */
+ --sidebar: oklch(0.16 0.015 240);
+ --sidebar-foreground: oklch(0.96 0.005 220);
+ --sidebar-primary: var(--dn-blue);
+ --sidebar-primary-foreground: oklch(0.12 0.02 240);
+ --sidebar-accent: oklch(0.22 0.02 240);
+ --sidebar-accent-foreground: oklch(0.96 0.005 220);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: var(--dn-blue);
+}
diff --git a/packages/admin-ui/src/utils/path.ts b/packages/admin-ui/src/utils/path.ts
new file mode 100644
index 0000000000..78ff89c6e7
--- /dev/null
+++ b/packages/admin-ui/src/utils/path.ts
@@ -0,0 +1,16 @@
+const LEGACY_BASE_PATH = "/legacy";
+
+/**
+ * Prefixes a legacy route fragment with the mounted legacy app base path.
+ *
+ * Examples:
+ * - withLegacyBase("packages/my") => "/staking/packages/my"
+ * - withLegacyBase("/packages/my") => "/staking/packages/my"
+ * - withLegacyBase("") => "/staking"
+ */
+export function withLegacyBase(path = ""): string {
+ const normalized = path.replace(/^\/+/, "");
+ return normalized ? `${LEGACY_BASE_PATH}/${normalized}` : LEGACY_BASE_PATH;
+}
+
+export { LEGACY_BASE_PATH };
diff --git a/packages/admin-ui/tsconfig.json b/packages/admin-ui/tsconfig.json
index 2ce73ef294..68f87aab9c 100644
--- a/packages/admin-ui/tsconfig.json
+++ b/packages/admin-ui/tsconfig.json
@@ -7,6 +7,9 @@
"moduleResolution": "node",
"baseUrl": "src",
+ "paths": {
+ "@/*": ["*"]
+ },
"types": ["vite/client"],
diff --git a/packages/admin-ui/vite.config.ts b/packages/admin-ui/vite.config.ts
index aa426eaa90..3eb2738130 100644
--- a/packages/admin-ui/vite.config.ts
+++ b/packages/admin-ui/vite.config.ts
@@ -1,5 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
import viteTsconfigPaths from "vite-tsconfig-paths";
import svgr from "vite-plugin-svgr";
@@ -10,7 +11,7 @@ export default defineConfig({
outDir: "build"
},
base: "/",
- plugins: [react(), viteTsconfigPaths(), svgr()],
+ plugins: [react(), tailwindcss(), viteTsconfigPaths(), svgr()],
server: {
// this ensures that the browser opens upon server start
//open: false,
diff --git a/yarn.lock b/yarn.lock
index dca2afbf44..c23074ff5d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -223,6 +223,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/code-frame@npm:^7.29.0":
+ version: 7.29.0
+ resolution: "@babel/code-frame@npm:7.29.0"
+ dependencies:
+ "@babel/helper-validator-identifier": "npm:^7.28.5"
+ js-tokens: "npm:^4.0.0"
+ picocolors: "npm:^1.1.1"
+ checksum: 10c0/d34cc504e7765dfb576a663d97067afb614525806b5cad1a5cc1a7183b916fec8ff57fa233585e3926fd5a9e6b31aae6df91aa81ae9775fb7a28f658d3346f0d
+ languageName: node
+ linkType: hard
+
"@babel/compat-data@npm:^7.24.7":
version: 7.24.7
resolution: "@babel/compat-data@npm:7.24.7"
@@ -283,6 +294,29 @@ __metadata:
languageName: node
linkType: hard
+"@babel/core@npm:^7.28.0":
+ version: 7.29.0
+ resolution: "@babel/core@npm:7.29.0"
+ dependencies:
+ "@babel/code-frame": "npm:^7.29.0"
+ "@babel/generator": "npm:^7.29.0"
+ "@babel/helper-compilation-targets": "npm:^7.28.6"
+ "@babel/helper-module-transforms": "npm:^7.28.6"
+ "@babel/helpers": "npm:^7.28.6"
+ "@babel/parser": "npm:^7.29.0"
+ "@babel/template": "npm:^7.28.6"
+ "@babel/traverse": "npm:^7.29.0"
+ "@babel/types": "npm:^7.29.0"
+ "@jridgewell/remapping": "npm:^2.3.5"
+ convert-source-map: "npm:^2.0.0"
+ debug: "npm:^4.1.0"
+ gensync: "npm:^1.0.0-beta.2"
+ json5: "npm:^2.2.3"
+ semver: "npm:^6.3.1"
+ checksum: 10c0/5127d2e8e842ae409e11bcbb5c2dff9874abf5415e8026925af7308e903f4f43397341467a130490d1a39884f461bc2b67f3063bce0be44340db89687fd852aa
+ languageName: node
+ linkType: hard
+
"@babel/generator@npm:^7.22.7":
version: 7.22.9
resolution: "@babel/generator@npm:7.22.9"
@@ -332,6 +366,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/generator@npm:^7.29.0":
+ version: 7.29.1
+ resolution: "@babel/generator@npm:7.29.1"
+ dependencies:
+ "@babel/parser": "npm:^7.29.0"
+ "@babel/types": "npm:^7.29.0"
+ "@jridgewell/gen-mapping": "npm:^0.3.12"
+ "@jridgewell/trace-mapping": "npm:^0.3.28"
+ jsesc: "npm:^3.0.2"
+ checksum: 10c0/349086e6876258ef3fb2823030fee0f6c0eb9c3ebe35fc572e16997f8c030d765f636ddc6299edae63e760ea6658f8ee9a2edfa6d6b24c9a80c917916b973551
+ languageName: node
+ linkType: hard
+
"@babel/helper-annotate-as-pure@npm:^7.18.6, @babel/helper-annotate-as-pure@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-annotate-as-pure@npm:7.22.5"
@@ -341,6 +388,15 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-annotate-as-pure@npm:^7.27.3":
+ version: 7.27.3
+ resolution: "@babel/helper-annotate-as-pure@npm:7.27.3"
+ dependencies:
+ "@babel/types": "npm:^7.27.3"
+ checksum: 10c0/94996ce0a05b7229f956033e6dcd69393db2b0886d0db6aff41e704390402b8cdcca11f61449cb4f86cfd9e61b5ad3a73e4fa661eeed7846b125bd1c33dbc633
+ languageName: node
+ linkType: hard
+
"@babel/helper-compilation-targets@npm:^7.24.7":
version: 7.24.7
resolution: "@babel/helper-compilation-targets@npm:7.24.7"
@@ -386,6 +442,23 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-create-class-features-plugin@npm:^7.28.6":
+ version: 7.28.6
+ resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6"
+ dependencies:
+ "@babel/helper-annotate-as-pure": "npm:^7.27.3"
+ "@babel/helper-member-expression-to-functions": "npm:^7.28.5"
+ "@babel/helper-optimise-call-expression": "npm:^7.27.1"
+ "@babel/helper-replace-supers": "npm:^7.28.6"
+ "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
+ "@babel/traverse": "npm:^7.28.6"
+ semver: "npm:^6.3.1"
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: 10c0/0b62b46717891f4366006b88c9b7f277980d4f578c4c3789b7a4f5a2e09e121de4cda9a414ab403986745cd3ad1af3fe2d948c9f78ab80d4dc085afc9602af50
+ languageName: node
+ linkType: hard
+
"@babel/helper-environment-visitor@npm:^7.22.20":
version: 7.22.20
resolution: "@babel/helper-environment-visitor@npm:7.22.20"
@@ -473,6 +546,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-member-expression-to-functions@npm:^7.28.5":
+ version: 7.28.5
+ resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5"
+ dependencies:
+ "@babel/traverse": "npm:^7.28.5"
+ "@babel/types": "npm:^7.28.5"
+ checksum: 10c0/4e6e05fbf4dffd0bc3e55e28fcaab008850be6de5a7013994ce874ec2beb90619cda4744b11607a60f8aae0227694502908add6188ceb1b5223596e765b44814
+ languageName: node
+ linkType: hard
+
"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-module-imports@npm:7.22.5"
@@ -539,6 +622,15 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-optimise-call-expression@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-optimise-call-expression@npm:7.27.1"
+ dependencies:
+ "@babel/types": "npm:^7.27.1"
+ checksum: 10c0/6b861e7fcf6031b9c9fc2de3cd6c005e94a459d6caf3621d93346b52774925800ca29d4f64595a5ceacf4d161eb0d27649ae385110ed69491d9776686fa488e6
+ languageName: node
+ linkType: hard
+
"@babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/helper-plugin-utils@npm:7.22.5"
@@ -553,6 +645,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.28.6":
+ version: 7.28.6
+ resolution: "@babel/helper-plugin-utils@npm:7.28.6"
+ checksum: 10c0/3f5f8acc152fdbb69a84b8624145ff4f9b9f6e776cb989f9f968f8606eb7185c5c3cfcf3ba08534e37e1e0e1c118ac67080610333f56baa4f7376c99b5f1143d
+ languageName: node
+ linkType: hard
+
"@babel/helper-replace-supers@npm:^7.22.9":
version: 7.22.9
resolution: "@babel/helper-replace-supers@npm:7.22.9"
@@ -566,6 +665,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-replace-supers@npm:^7.28.6":
+ version: 7.28.6
+ resolution: "@babel/helper-replace-supers@npm:7.28.6"
+ dependencies:
+ "@babel/helper-member-expression-to-functions": "npm:^7.28.5"
+ "@babel/helper-optimise-call-expression": "npm:^7.27.1"
+ "@babel/traverse": "npm:^7.28.6"
+ peerDependencies:
+ "@babel/core": ^7.0.0
+ checksum: 10c0/04663c6389551b99b8c3e7ba4e2638b8ca2a156418c26771516124c53083aa8e74b6a45abe5dd46360af79709a0e9c6b72c076d0eab9efecdd5aaf836e79d8d5
+ languageName: node
+ linkType: hard
+
"@babel/helper-simple-access@npm:^7.24.7":
version: 7.24.7
resolution: "@babel/helper-simple-access@npm:7.24.7"
@@ -585,6 +697,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1"
+ dependencies:
+ "@babel/traverse": "npm:^7.27.1"
+ "@babel/types": "npm:^7.27.1"
+ checksum: 10c0/f625013bcdea422c470223a2614e90d2c1cc9d832e97f32ca1b4f82b34bb4aa67c3904cb4b116375d3b5b753acfb3951ed50835a1e832e7225295c7b0c24dff7
+ languageName: node
+ linkType: hard
+
"@babel/helper-split-export-declaration@npm:^7.22.6":
version: 7.22.6
resolution: "@babel/helper-split-export-declaration@npm:7.22.6"
@@ -761,6 +883,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/parser@npm:^7.28.0, @babel/parser@npm:^7.29.0":
+ version: 7.29.0
+ resolution: "@babel/parser@npm:7.29.0"
+ dependencies:
+ "@babel/types": "npm:^7.29.0"
+ bin:
+ parser: ./bin/babel-parser.js
+ checksum: 10c0/333b2aa761264b91577a74bee86141ef733f9f9f6d4fc52548e4847dc35dfbf821f58c46832c637bfa761a6d9909d6a68f7d1ed59e17e4ffbb958dc510c17b62
+ languageName: node
+ linkType: hard
+
"@babel/parser@npm:^7.28.6":
version: 7.28.6
resolution: "@babel/parser@npm:7.28.6"
@@ -797,6 +930,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-syntax-jsx@npm:^7.27.1":
+ version: 7.28.6
+ resolution: "@babel/plugin-syntax-jsx@npm:7.28.6"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.28.6"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10c0/b98fc3cd75e4ca3d5ca1162f610c286e14ede1486e0d297c13a5eb0ac85680ac9656d17d348bddd9160a54d797a08cea5eaac02b9330ddebb7b26732b7b99fb5
+ languageName: node
+ linkType: hard
+
"@babel/plugin-syntax-private-property-in-object@npm:^7.14.5":
version: 7.14.5
resolution: "@babel/plugin-syntax-private-property-in-object@npm:7.14.5"
@@ -808,6 +952,29 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-syntax-typescript@npm:^7.28.6":
+ version: 7.28.6
+ resolution: "@babel/plugin-syntax-typescript@npm:7.28.6"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.28.6"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10c0/b0c392a35624883ac480277401ac7d92d8646b66e33639f5d350de7a6723924265985ae11ab9ebd551740ded261c443eaa9a87ea19def9763ca1e0d78c97dea8
+ languageName: node
+ linkType: hard
+
+"@babel/plugin-transform-modules-commonjs@npm:^7.27.1":
+ version: 7.28.6
+ resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6"
+ dependencies:
+ "@babel/helper-module-transforms": "npm:^7.28.6"
+ "@babel/helper-plugin-utils": "npm:^7.28.6"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10c0/7c45992797c6150644c8552feff4a016ba7bd6d59ff2b039ed969a9c5b20a6804cd9d21db5045fc8cca8ca7f08262497e354e93f8f2be6a1cdf3fbfa8c31a9b6
+ languageName: node
+ linkType: hard
+
"@babel/plugin-transform-react-jsx-self@npm:^7.24.5":
version: 7.24.7
resolution: "@babel/plugin-transform-react-jsx-self@npm:7.24.7"
@@ -830,6 +997,36 @@ __metadata:
languageName: node
linkType: hard
+"@babel/plugin-transform-typescript@npm:^7.28.0, @babel/plugin-transform-typescript@npm:^7.28.5":
+ version: 7.28.6
+ resolution: "@babel/plugin-transform-typescript@npm:7.28.6"
+ dependencies:
+ "@babel/helper-annotate-as-pure": "npm:^7.27.3"
+ "@babel/helper-create-class-features-plugin": "npm:^7.28.6"
+ "@babel/helper-plugin-utils": "npm:^7.28.6"
+ "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
+ "@babel/plugin-syntax-typescript": "npm:^7.28.6"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10c0/72dbfd3e5f71c4e30445e610758ec0eef65347fafd72bd46f4903733df0d537663a72a81c1626f213a0feab7afc68ba83f1648ffece888dd0868115c9cb748f6
+ languageName: node
+ linkType: hard
+
+"@babel/preset-typescript@npm:^7.27.1":
+ version: 7.28.5
+ resolution: "@babel/preset-typescript@npm:7.28.5"
+ dependencies:
+ "@babel/helper-plugin-utils": "npm:^7.27.1"
+ "@babel/helper-validator-option": "npm:^7.27.1"
+ "@babel/plugin-syntax-jsx": "npm:^7.27.1"
+ "@babel/plugin-transform-modules-commonjs": "npm:^7.27.1"
+ "@babel/plugin-transform-typescript": "npm:^7.28.5"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10c0/b3d55548854c105085dd80f638147aa8295bc186d70492289242d6c857cb03a6c61ec15186440ea10ed4a71cdde7d495f5eb3feda46273f36b0ac926e8409629
+ languageName: node
+ linkType: hard
+
"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.22.6
resolution: "@babel/runtime@npm:7.22.6"
@@ -953,6 +1150,21 @@ __metadata:
languageName: node
linkType: hard
+"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.29.0":
+ version: 7.29.0
+ resolution: "@babel/traverse@npm:7.29.0"
+ dependencies:
+ "@babel/code-frame": "npm:^7.29.0"
+ "@babel/generator": "npm:^7.29.0"
+ "@babel/helper-globals": "npm:^7.28.0"
+ "@babel/parser": "npm:^7.29.0"
+ "@babel/template": "npm:^7.28.6"
+ "@babel/types": "npm:^7.29.0"
+ debug: "npm:^4.3.1"
+ checksum: 10c0/f63ef6e58d02a9fbf3c0e2e5f1c877da3e0bc57f91a19d2223d53e356a76859cbaf51171c9211c71816d94a0e69efa2732fd27ffc0e1bbc84b636e60932333eb
+ languageName: node
+ linkType: hard
+
"@babel/traverse@npm:^7.28.6":
version: 7.28.6
resolution: "@babel/traverse@npm:7.28.6"
@@ -1011,6 +1223,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.5, @babel/types@npm:^7.29.0":
+ version: 7.29.0
+ resolution: "@babel/types@npm:7.29.0"
+ dependencies:
+ "@babel/helper-string-parser": "npm:^7.27.1"
+ "@babel/helper-validator-identifier": "npm:^7.28.5"
+ checksum: 10c0/23cc3466e83bcbfab8b9bd0edaafdb5d4efdb88b82b3be6728bbade5ba2f0996f84f63b1c5f7a8c0d67efded28300898a5f930b171bb40b311bca2029c4e9b4f
+ languageName: node
+ linkType: hard
+
"@babel/types@npm:^7.8.3":
version: 7.25.2
resolution: "@babel/types@npm:7.25.2"
@@ -1108,6 +1330,7 @@ __metadata:
"@grafana/faro-web-tracing": "npm:^2.3.1"
"@reduxjs/toolkit": "npm:^1.3.5"
"@rollup/rollup-linux-x64-gnu": "npm:4.18.0"
+ "@tailwindcss/vite": "npm:^4.2.1"
"@types/clipboard": "npm:^2.0.7"
"@types/jest": "npm:^24.9.1"
"@types/qrcode.react": "npm:^1.0.2"
@@ -1121,16 +1344,22 @@ __metadata:
"@vitejs/plugin-react": "npm:^4.3.1"
ajv: "npm:^6.10.2"
bootstrap: "npm:^5.3"
+ class-variance-authority: "npm:^0.7.1"
clipboard: "npm:^2.0.1"
+ clsx: "npm:^2.1.1"
deepmerge: "npm:^2.1.1"
+ embla-carousel-react: "npm:^8.6.0"
ethereum-blockies-base64: "npm:^1.0.2"
is-ipfs: "npm:^8.0.1"
lodash-es: "npm:^4.17.21"
+ lucide-react: "npm:^0.577.0"
mitt: "npm:^2.1.0"
+ next-themes: "npm:^0.4.6"
nodemon: "npm:^3.1.4"
prettier: "npm:^1.16.4"
pretty-bytes: "npm:^5.3.0"
qrcode.react: "npm:^0.8.0"
+ radix-ui: "npm:^1.4.3"
react: "npm:^18.2.0"
react-bootstrap: "npm:^2.10"
react-dom: "npm:^18.3.1"
@@ -1144,10 +1373,16 @@ __metadata:
redux-thunk: "npm:^2.3.0"
sass: "npm:^1.49.7"
semver: "npm:^7.3.8"
+ shadcn: "npm:^4.0.8"
socket.io-client: "npm:^4.5.1"
+ sonner: "npm:^2.0.7"
styled-components: "npm:^4.2.0"
swr: "npm:^0.2.0"
+ tailwind-merge: "npm:^3.5.0"
+ tailwindcss: "npm:^4.2.1"
ts-node: "npm:^10.9.2"
+ tw-animate-css: "npm:^1.4.0"
+ vaul: "npm:^1.1.2"
vite: "npm:^5.4.19"
vite-plugin-svgr: "npm:^4.5.0"
vite-tsconfig-paths: "npm:^4.3.2"
@@ -1623,6 +1858,62 @@ __metadata:
languageName: unknown
linkType: soft
+"@dotenvx/dotenvx@npm:^1.48.4":
+ version: 1.55.1
+ resolution: "@dotenvx/dotenvx@npm:1.55.1"
+ dependencies:
+ commander: "npm:^11.1.0"
+ dotenv: "npm:^17.2.1"
+ eciesjs: "npm:^0.4.10"
+ execa: "npm:^5.1.1"
+ fdir: "npm:^6.2.0"
+ ignore: "npm:^5.3.0"
+ object-treeify: "npm:1.1.33"
+ picomatch: "npm:^4.0.2"
+ which: "npm:^4.0.0"
+ bin:
+ dotenvx: src/cli/dotenvx.js
+ checksum: 10c0/e1c392269770643dda2746c733456a2cee299d83928ae03babdc0952cb3dc4e61361f0fcb8217e3c75bd3ef282c33caeac4172e910b2b4107fc5b2c1472e8069
+ languageName: node
+ linkType: hard
+
+"@ecies/ciphers@npm:^0.2.5":
+ version: 0.2.5
+ resolution: "@ecies/ciphers@npm:0.2.5"
+ peerDependencies:
+ "@noble/ciphers": ^1.0.0
+ checksum: 10c0/fcc08327216d225310596dc5d6a25da919e641e271c1895384e068fdd910e835271a103c5105aaa8ea24b33931b7d1975341b044919d38fd586e8ad8e0f33be6
+ languageName: node
+ linkType: hard
+
+"@emnapi/core@npm:^1.7.1, @emnapi/core@npm:^1.8.1":
+ version: 1.8.1
+ resolution: "@emnapi/core@npm:1.8.1"
+ dependencies:
+ "@emnapi/wasi-threads": "npm:1.1.0"
+ tslib: "npm:^2.4.0"
+ checksum: 10c0/2c242f4b49779bac403e1cbcc98edacdb1c8ad36562408ba9a20663824669e930bc8493be46a2522d9dc946b8d96cd7073970bae914928c7671b5221c85b432e
+ languageName: node
+ linkType: hard
+
+"@emnapi/runtime@npm:^1.7.1, @emnapi/runtime@npm:^1.8.1":
+ version: 1.8.1
+ resolution: "@emnapi/runtime@npm:1.8.1"
+ dependencies:
+ tslib: "npm:^2.4.0"
+ checksum: 10c0/f4929d75e37aafb24da77d2f58816761fe3f826aad2e37fa6d4421dac9060cbd5098eea1ac3c9ecc4526b89deb58153852fa432f87021dc57863f2ff726d713f
+ languageName: node
+ linkType: hard
+
+"@emnapi/wasi-threads@npm:1.1.0, @emnapi/wasi-threads@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "@emnapi/wasi-threads@npm:1.1.0"
+ dependencies:
+ tslib: "npm:^2.4.0"
+ checksum: 10c0/e6d54bf2b1e64cdd83d2916411e44e579b6ae35d5def0dea61a3c452d9921373044dff32a8b8473ae60c80692bdc39323e98b96a3f3d87ba6886b24dd0ef7ca1
+ languageName: node
+ linkType: hard
+
"@emotion/is-prop-valid@npm:^0.8.1":
version: 0.8.8
resolution: "@emotion/is-prop-valid@npm:0.8.8"
@@ -2578,6 +2869,44 @@ __metadata:
languageName: node
linkType: hard
+"@floating-ui/core@npm:^1.7.5":
+ version: 1.7.5
+ resolution: "@floating-ui/core@npm:1.7.5"
+ dependencies:
+ "@floating-ui/utils": "npm:^0.2.11"
+ checksum: 10c0/f9c52205e198b231d63a387b09c659aab08c46a1899e0b0bbe147b8b4f048b546f15ba17cb5d2a471da9534f1883d979425e13e5c4ceee67be63e4b0abd4db5d
+ languageName: node
+ linkType: hard
+
+"@floating-ui/dom@npm:^1.7.6":
+ version: 1.7.6
+ resolution: "@floating-ui/dom@npm:1.7.6"
+ dependencies:
+ "@floating-ui/core": "npm:^1.7.5"
+ "@floating-ui/utils": "npm:^0.2.11"
+ checksum: 10c0/5c098e0d7b58c9bc769f276cca1766994c2c9c70c92d091a61bba8b3e9be53c011e0a79a8457fc2fb2f3d91697a26eb52e0a4962ef936dc963b45f58613c212f
+ languageName: node
+ linkType: hard
+
+"@floating-ui/react-dom@npm:^2.0.0":
+ version: 2.1.8
+ resolution: "@floating-ui/react-dom@npm:2.1.8"
+ dependencies:
+ "@floating-ui/dom": "npm:^1.7.6"
+ peerDependencies:
+ react: ">=16.8.0"
+ react-dom: ">=16.8.0"
+ checksum: 10c0/26260ca4bb23b57c73b824062505abf977a008ce6e0463bdacca74f7e49853c4cd1d2bbf1a77c6caa17fa37dfffda2c6c4cd07a8737ebd7474aaff7818401d75
+ languageName: node
+ linkType: hard
+
+"@floating-ui/utils@npm:^0.2.11":
+ version: 0.2.11
+ resolution: "@floating-ui/utils@npm:0.2.11"
+ checksum: 10c0/f4bcea1559bdbb721ecc8e8ead423ac58d6a5b6e70b602cf0810ba6ad4ed1c77211b207faa88b278a9042f0c743133de08a203ed6741c1b6443423332884d5b3
+ languageName: node
+ linkType: hard
+
"@grafana/faro-core@npm:^2.3.1":
version: 2.3.1
resolution: "@grafana/faro-core@npm:2.3.1"
@@ -2769,6 +3098,15 @@ __metadata:
languageName: node
linkType: hard
+"@hono/node-server@npm:^1.19.9":
+ version: 1.19.11
+ resolution: "@hono/node-server@npm:1.19.11"
+ peerDependencies:
+ hono: ^4
+ checksum: 10c0/34b1c29c249c5cd95469980b5c359370f3cbab49b3603f324a4afbf895d68b8d5485c71f1887769eabeb3499276c49e7102084234b4feb3853edb748aaa85f50
+ languageName: node
+ linkType: hard
+
"@humanfs/core@npm:^0.19.1":
version: 0.19.1
resolution: "@humanfs/core@npm:0.19.1"
@@ -2807,6 +3145,68 @@ __metadata:
languageName: node
linkType: hard
+"@inquirer/ansi@npm:^1.0.2":
+ version: 1.0.2
+ resolution: "@inquirer/ansi@npm:1.0.2"
+ checksum: 10c0/8e408cc628923aa93402e66657482ccaa2ad5174f9db526d9a8b443f9011e9cd8f70f0f534f5fe3857b8a9df3bce1e25f66c96f666d6750490bd46e2b4f3b829
+ languageName: node
+ linkType: hard
+
+"@inquirer/confirm@npm:^5.0.0":
+ version: 5.1.21
+ resolution: "@inquirer/confirm@npm:5.1.21"
+ dependencies:
+ "@inquirer/core": "npm:^10.3.2"
+ "@inquirer/type": "npm:^3.0.10"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/a95bbdbb17626c484735a4193ed6b6a6fbb078cf62116ec8e1667f647e534dd6618e688ecc7962585efcc56881b544b8c53db3914599bbf2ab842e7f224b0fca
+ languageName: node
+ linkType: hard
+
+"@inquirer/core@npm:^10.3.2":
+ version: 10.3.2
+ resolution: "@inquirer/core@npm:10.3.2"
+ dependencies:
+ "@inquirer/ansi": "npm:^1.0.2"
+ "@inquirer/figures": "npm:^1.0.15"
+ "@inquirer/type": "npm:^3.0.10"
+ cli-width: "npm:^4.1.0"
+ mute-stream: "npm:^2.0.0"
+ signal-exit: "npm:^4.1.0"
+ wrap-ansi: "npm:^6.2.0"
+ yoctocolors-cjs: "npm:^2.1.3"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/f0f27e07fe288e01e3949b4ad216c19751f025ce77c610366e08d8b0f7a135d064dc074732031d251584b454c576f1e5c849e4abe259186dd5d4974c8f85c13e
+ languageName: node
+ linkType: hard
+
+"@inquirer/figures@npm:^1.0.15":
+ version: 1.0.15
+ resolution: "@inquirer/figures@npm:1.0.15"
+ checksum: 10c0/6e39a040d260ae234ae220180b7994ff852673e20be925f8aa95e78c7934d732b018cbb4d0ec39e600a410461bcb93dca771e7de23caa10630d255692e440f69
+ languageName: node
+ linkType: hard
+
+"@inquirer/type@npm:^3.0.10":
+ version: 3.0.10
+ resolution: "@inquirer/type@npm:3.0.10"
+ peerDependencies:
+ "@types/node": ">=18"
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ checksum: 10c0/a846c7a570e3bf2657d489bcc5dcdc3179d24c7323719de1951dcdb722400ac76e5b2bfe9765d0a789bc1921fac810983d7999f021f30a78a6a174c23fc78dc9
+ languageName: node
+ linkType: hard
+
"@ipld/car@npm:^5.4.0":
version: 5.4.0
resolution: "@ipld/car@npm:5.4.0"
@@ -3000,7 +3400,7 @@ __metadata:
languageName: node
linkType: hard
-"@jridgewell/sourcemap-codec@npm:^1.5.0":
+"@jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5":
version: 1.5.5
resolution: "@jridgewell/sourcemap-codec@npm:1.5.5"
checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0
@@ -3249,6 +3649,53 @@ __metadata:
languageName: node
linkType: hard
+"@modelcontextprotocol/sdk@npm:^1.26.0":
+ version: 1.27.1
+ resolution: "@modelcontextprotocol/sdk@npm:1.27.1"
+ dependencies:
+ "@hono/node-server": "npm:^1.19.9"
+ ajv: "npm:^8.17.1"
+ ajv-formats: "npm:^3.0.1"
+ content-type: "npm:^1.0.5"
+ cors: "npm:^2.8.5"
+ cross-spawn: "npm:^7.0.5"
+ eventsource: "npm:^3.0.2"
+ eventsource-parser: "npm:^3.0.0"
+ express: "npm:^5.2.1"
+ express-rate-limit: "npm:^8.2.1"
+ hono: "npm:^4.11.4"
+ jose: "npm:^6.1.3"
+ json-schema-typed: "npm:^8.0.2"
+ pkce-challenge: "npm:^5.0.0"
+ raw-body: "npm:^3.0.0"
+ zod: "npm:^3.25 || ^4.0"
+ zod-to-json-schema: "npm:^3.25.1"
+ peerDependencies:
+ "@cfworker/json-schema": ^4.1.1
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ "@cfworker/json-schema":
+ optional: true
+ zod:
+ optional: false
+ checksum: 10c0/1b8ad87093c9e43174c7d65864b3d826a8dd050d5c32248f5da49fd72c51b556ebd702e3e49a7f1cc7fa25717a3f7fcee22ed89edd5bd3d8f4e1f8ca499b365e
+ languageName: node
+ linkType: hard
+
+"@mswjs/interceptors@npm:^0.41.2":
+ version: 0.41.3
+ resolution: "@mswjs/interceptors@npm:0.41.3"
+ dependencies:
+ "@open-draft/deferred-promise": "npm:^2.2.0"
+ "@open-draft/logger": "npm:^0.3.0"
+ "@open-draft/until": "npm:^2.0.0"
+ is-node-process: "npm:^1.2.0"
+ outvariant: "npm:^1.4.3"
+ strict-event-emitter: "npm:^0.5.1"
+ checksum: 10c0/a259bbfc3bb4caada7a9a3529cc830159818e838c152df89ac890e7203df615a5e070ca63aa1e70a39868322ff5c1441ab74bbadb4081ca55b0c3a462e2903c0
+ languageName: node
+ linkType: hard
+
"@multiformats/dns@npm:^1.0.3":
version: 1.0.6
resolution: "@multiformats/dns@npm:1.0.6"
@@ -3374,6 +3821,24 @@ __metadata:
languageName: node
linkType: hard
+"@napi-rs/wasm-runtime@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "@napi-rs/wasm-runtime@npm:1.1.1"
+ dependencies:
+ "@emnapi/core": "npm:^1.7.1"
+ "@emnapi/runtime": "npm:^1.7.1"
+ "@tybys/wasm-util": "npm:^0.10.1"
+ checksum: 10c0/04d57b67e80736e41fe44674a011878db0a8ad893f4d44abb9d3608debb7c174224cba2796ed5b0c1d367368159f3ca6be45f1c59222f70e32ddc880f803d447
+ languageName: node
+ linkType: hard
+
+"@noble/ciphers@npm:^1.3.0":
+ version: 1.3.0
+ resolution: "@noble/ciphers@npm:1.3.0"
+ checksum: 10c0/3ba6da645ce45e2f35e3b2e5c87ceba86b21dfa62b9466ede9edfb397f8116dae284f06652c0cd81d99445a2262b606632e868103d54ecc99fd946ae1af8cd37
+ languageName: node
+ linkType: hard
+
"@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0":
version: 1.1.0
resolution: "@noble/curves@npm:1.1.0"
@@ -3401,6 +3866,15 @@ __metadata:
languageName: node
linkType: hard
+"@noble/curves@npm:^1.9.7":
+ version: 1.9.7
+ resolution: "@noble/curves@npm:1.9.7"
+ dependencies:
+ "@noble/hashes": "npm:1.8.0"
+ checksum: 10c0/150014751ebe8ca06a8654ca2525108452ea9ee0be23430332769f06808cddabfe84f248b6dbf836916bc869c27c2092957eec62c7506d68a1ed0a624017c2a3
+ languageName: node
+ linkType: hard
+
"@noble/ed25519@npm:^1.6.0":
version: 1.7.3
resolution: "@noble/ed25519@npm:1.7.3"
@@ -3422,7 +3896,7 @@ __metadata:
languageName: node
linkType: hard
-"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.6.1":
+"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.6.1, @noble/hashes@npm:^1.8.0":
version: 1.8.0
resolution: "@noble/hashes@npm:1.8.0"
checksum: 10c0/06a0b52c81a6fa7f04d67762e08b2c476a00285858150caeaaff4037356dd5e119f45b2a530f638b77a5eeca013168ec1b655db41bae3236cb2e9d511484fc77
@@ -3485,6 +3959,30 @@ __metadata:
languageName: node
linkType: hard
+"@open-draft/deferred-promise@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "@open-draft/deferred-promise@npm:2.2.0"
+ checksum: 10c0/eafc1b1d0fc8edb5e1c753c5e0f3293410b40dde2f92688211a54806d4136887051f39b98c1950370be258483deac9dfd17cf8b96557553765198ef2547e4549
+ languageName: node
+ linkType: hard
+
+"@open-draft/logger@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "@open-draft/logger@npm:0.3.0"
+ dependencies:
+ is-node-process: "npm:^1.2.0"
+ outvariant: "npm:^1.4.0"
+ checksum: 10c0/90010647b22e9693c16258f4f9adb034824d1771d3baa313057b9a37797f571181005bc50415a934eaf7c891d90ff71dcd7a9d5048b0b6bb438f31bef2c7c5c1
+ languageName: node
+ linkType: hard
+
+"@open-draft/until@npm:^2.0.0":
+ version: 2.1.0
+ resolution: "@open-draft/until@npm:2.1.0"
+ checksum: 10c0/61d3f99718dd86bb393fee2d7a785f961dcaf12f2055f0c693b27f4d0cd5f7a03d498a6d9289773b117590d794a43cd129366fd8e99222e4832f67b1653d54cf
+ languageName: node
+ linkType: hard
+
"@opentelemetry/api-logs@npm:0.213.0":
version: 0.213.0
resolution: "@opentelemetry/api-logs@npm:0.213.0"
@@ -3802,18 +4300,1313 @@ __metadata:
languageName: node
linkType: hard
-"@react-aria/ssr@npm:^3.5.0":
- version: 3.9.10
- resolution: "@react-aria/ssr@npm:3.9.10"
- dependencies:
- "@swc/helpers": "npm:^0.5.0"
- peerDependencies:
- react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
- checksum: 10c0/44acb4c441d9c5d65aab94aa81fd8368413cf2958ab458582296dd78f6ba4783583f2311fa986120060e5c26b54b1f01e8910ffd17e4f41ccc5fc8c357d84089
+"@radix-ui/number@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/number@npm:1.1.1"
+ checksum: 10c0/0570ad92287398e8a7910786d7cee0a998174cdd6637ba61571992897c13204adf70b9ed02d0da2af554119411128e701d9c6b893420612897b438dc91db712b
languageName: node
linkType: hard
-"@redux-saga/core@npm:^1.0.0":
+"@radix-ui/primitive@npm:1.1.3":
+ version: 1.1.3
+ resolution: "@radix-ui/primitive@npm:1.1.3"
+ checksum: 10c0/88860165ee7066fa2c179f32ffcd3ee6d527d9dcdc0e8be85e9cb0e2c84834be8e3c1a976c74ba44b193f709544e12f54455d892b28e32f0708d89deda6b9f1d
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-accessible-icon@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-accessible-icon@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/2a912454b3f5e1dbea599747be39e94a7d23b1d7c4261fd20b04faf38db9aaf00f4c26fc96922d75871e57a0f94948fe60ec044d3022c934b8df43da94faf18a
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-accordion@npm:1.2.12":
+ version: 1.2.12
+ resolution: "@radix-ui/react-accordion@npm:1.2.12"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collapsible": "npm:1.1.12"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/c64a53ce766a1ef529cf6413ed7382598c94f78879b3a83ceda27cb1894ed6eb6e8ad61f6a550ca3c7fa813657045dadfc7328dbf1d736a37e1cf3c446db43de
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-alert-dialog@npm:1.1.15":
+ version: 1.1.15
+ resolution: "@radix-ui/react-alert-dialog@npm:1.1.15"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dialog": "npm:1.1.15"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/038de84ad1b36c162e5f5a3b4034de95558698eb6e3f483d2b1a15f4a502c921c4e6a5a723fe6f29e928ed7001ffe38ac6fd16bb720b1e629892ce7beb1da174
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-arrow@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-arrow@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/c3b46766238b3ee2a394d8806a5141432361bf1425110c9f0dcf480bda4ebd304453a53f294b5399c6ee3ccfcae6fd544921fd01ddc379cf5942acdd7168664b
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-aspect-ratio@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-aspect-ratio@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/12761718749d56393b6135e01434f8384dd05bcebf2c2fedc04f85f414174297d36531e17010df9f40aec7407c76d683e3f309ce5a39536ed1a3e03a12d08f71
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-avatar@npm:1.1.10":
+ version: 1.1.10
+ resolution: "@radix-ui/react-avatar@npm:1.1.10"
+ dependencies:
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-is-hydrated": "npm:0.1.0"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/9fb0cf9a9d0fdbeaa2efda476402fc09db2e6ff9cd9aa3ea1d315d9c9579840722a4833725cb196c455e0bd775dfe04221a4f6855685ce89d2133c42e2b07e5f
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-checkbox@npm:1.3.3":
+ version: 1.3.3
+ resolution: "@radix-ui/react-checkbox@npm:1.3.3"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/5eeb78e37a6c9611a638a80b309c931dd6f1f8968357ab2abb453505392fa1397491441447ca2d5f4381faaac7fab2dc84c780e8ce27d931bd203fa014088b74
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-collapsible@npm:1.1.12":
+ version: 1.1.12
+ resolution: "@radix-ui/react-collapsible@npm:1.1.12"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/777cced73fbbec9cfafe6325aa5605e90f49d889af2778f4c4a6be101c07cacd69ae817d0b41cc27e3181f49392e2c06db7f32d6b084db047a51805ec70729b3
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-collection@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-collection@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/fa321a7300095508491f75414f02b243f0c3f179dc0728cfd115e2ea9f6f48f1516532b59f526d9ac81bbab63cd98a052074b4703ec0b9428fac945ebabec5fd
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-compose-refs@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-compose-refs@npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/d36a9c589eb75d634b9b139c80f916aadaf8a68a7c1c4b8c6c6b88755af1a92f2e343457042089f04cc3f23073619d08bb65419ced1402e9d4e299576d970771
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-context-menu@npm:2.2.16":
+ version: 2.2.16
+ resolution: "@radix-ui/react-context-menu@npm:2.2.16"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/950f7559e65474a19145238cf44d744cb1e49be2221ff18436ba49b496b05ccf93bd3906aaa2c7ab76bc77daf694911a78442801e0053f57d2e57ebbfd281c49
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-context@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-context@npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/cece731f8cc25d494c6589cc681e5c01a93867d895c75889973afa1a255f163c286e390baa7bc028858eaabe9f6b57270d0ca6377356f652c5557c1c7a41ccce
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-dialog@npm:1.1.15, @radix-ui/react-dialog@npm:^1.1.1":
+ version: 1.1.15
+ resolution: "@radix-ui/react-dialog@npm:1.1.15"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/2f2c88e3c281acaea2fd9b96fa82132d59177d3aa5da2e7c045596fd4028e84e44ac52ac28f4f236910605dd7d9338c2858ba44a9ced2af2e3e523abbfd33014
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-direction@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-direction@npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/7a89d9291f846a3105e45f4df98d6b7a08f8d7b30acdcd253005dc9db107ee83cbbebc9e47a9af1e400bcd47697f1511ceab23a399b0da854488fc7220482ac9
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-dismissable-layer@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-dismissable-layer@npm:1.1.11"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-escape-keydown": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/c825572a64073c4d3853702029979f6658770ffd6a98eabc4984e1dee1b226b4078a2a4dc7003f96475b438985e9b21a58e75f51db74dd06848dcae1f2d395dc
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-dropdown-menu@npm:2.1.16":
+ version: 2.1.16
+ resolution: "@radix-ui/react-dropdown-menu@npm:2.1.16"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/8caaa8dd791ccb284568720adafa59855e13860aa29eb20e10a04ba671cbbfa519a4c5d3a339a4d9fb08009eeb1065f4a8b5c3c8ef45e9753161cc560106b935
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-focus-guards@npm:1.1.3":
+ version: 1.1.3
+ resolution: "@radix-ui/react-focus-guards@npm:1.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/0bab65eb8d7e4f72f685d63de7fbba2450e3cb15ad6a20a16b42195e9d335c576356f5a47cb58d1ffc115393e46d7b14b12c5d4b10029b0ec090861255866985
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-focus-scope@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-focus-scope@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/8a6071331bdeeb79b223463de75caf759b8ad19339cab838e537b8dbb2db236891a1f4df252445c854d375d43d9d315dfcce0a6b01553a2984ec372bb8f1300e
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-form@npm:0.1.8":
+ version: 0.1.8
+ resolution: "@radix-ui/react-form@npm:0.1.8"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-label": "npm:2.1.7"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/4245a16c9f638f625de2825889f41a5a32e7b1f3dfb785fa78981375b83b551718f69f84d8ea8da1dac0631835cd1fcd6fa3488415a31b3d38c4699a626e54c0
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-hover-card@npm:1.1.15":
+ version: 1.1.15
+ resolution: "@radix-ui/react-hover-card@npm:1.1.15"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/c44ab88b0c62a3c1bf274b72e5cc3f5a6aea571a52bf2fcb2d471e1336738adabdbd10c26c8e72071cad444704ac28fcf2679d43132b69279564ad689839cf4e
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-id@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-id@npm:1.1.1"
+ dependencies:
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/7d12e76818763d592c331277ef62b197e2e64945307e650bd058f0090e5ae48bbd07691b23b7e9e977901ef4eadcb3e2d5eaeb17a13859083384be83fc1292c7
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-label@npm:2.1.7":
+ version: 2.1.7
+ resolution: "@radix-ui/react-label@npm:2.1.7"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/d8c81411d5327b6db5cbf4b900bfcc52030315539911701cf8d82b4970aed80cbd66df5b62d2242859572c666cf4b0e147a8b39dc3c04bd024a4b4405e1183fe
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-menu@npm:2.1.16":
+ version: 2.1.16
+ resolution: "@radix-ui/react-menu@npm:2.1.16"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/27516b2b987fa9181c4da8645000af8f60691866a349d7a46b9505fa7d2e9d92b9e364db4f7305d08e9e57d0e1afc8df8354f8ee3c12aa05c0100c16b0e76c27
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-menubar@npm:1.1.16":
+ version: 1.1.16
+ resolution: "@radix-ui/react-menubar@npm:1.1.16"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/81f8134a178b8e81522280988dfa0327ddbb777e0b0650fb71dd2528b94af18c2a23166fd3497d17473fd3191e5317adda449248fd16945d786728c57ded097e
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-navigation-menu@npm:1.2.14":
+ version: 1.2.14
+ resolution: "@radix-ui/react-navigation-menu@npm:1.2.14"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/c06ca983fc99bf37f09101b1f423def413e5b7437308e519c878180411a12f837a6a3b293b873293b06e68ca60294597da0f2bf0321efaf9028ab4144e13187e
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-one-time-password-field@npm:0.1.8":
+ version: 0.1.8
+ resolution: "@radix-ui/react-one-time-password-field@npm:0.1.8"
+ dependencies:
+ "@radix-ui/number": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-effect-event": "npm:0.0.2"
+ "@radix-ui/react-use-is-hydrated": "npm:0.1.0"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/1dded300690d148c71b022ef2fffd271faae906ad6546ec39ae537d5f7cfaab76b8c13567c97660bc3623c38eb1686e248e6a442de289f70226df87230196650
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-password-toggle-field@npm:0.1.3":
+ version: 0.1.3
+ resolution: "@radix-ui/react-password-toggle-field@npm:0.1.3"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-effect-event": "npm:0.0.2"
+ "@radix-ui/react-use-is-hydrated": "npm:0.1.0"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/9c4eea5f6a05401e6c77d9bfebfea801c027c0e5a85bac5df71e6982b1162b79dbdeed6e6e52fc7fd2b34533ccc64c63ec47fbf5f9bf621dfd0d1810baec2f4d
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-popover@npm:1.1.15":
+ version: 1.1.15
+ resolution: "@radix-ui/react-popover@npm:1.1.15"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/c1c76b5e5985b128d03b621424fb453f769931d497759a1977734d303007da9f970570cf3ea1f6968ab609ab4a97f384168bff056197bd2d3d422abea0e3614b
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-popper@npm:1.2.8":
+ version: 1.2.8
+ resolution: "@radix-ui/react-popper@npm:1.2.8"
+ dependencies:
+ "@floating-ui/react-dom": "npm:^2.0.0"
+ "@radix-ui/react-arrow": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-rect": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ "@radix-ui/rect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/48e3f13eac3b8c13aca8ded37d74db17e1bb294da8d69f142ab6b8719a06c3f90051668bed64520bf9f3abdd77b382ce7ce209d056bb56137cecc949b69b421c
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-portal@npm:1.1.9":
+ version: 1.1.9
+ resolution: "@radix-ui/react-portal@npm:1.1.9"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/45b432497c722720c72c493a29ef6085bc84b50eafe79d48b45c553121b63e94f9cdb77a3a74b9c49126f8feb3feee009fe400d48b7759d3552396356b192cd7
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-presence@npm:1.1.5":
+ version: 1.1.5
+ resolution: "@radix-ui/react-presence@npm:1.1.5"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/d0e61d314250eeaef5369983cb790701d667f51734bafd98cf759072755562018052c594e6cdc5389789f4543cb0a4d98f03ff4e8f37338d6b5bf51a1700c1d1
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-primitive@npm:2.1.3":
+ version: 2.1.3
+ resolution: "@radix-ui/react-primitive@npm:2.1.3"
+ dependencies:
+ "@radix-ui/react-slot": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/fdff9b84913bb4172ef6d3af7442fca5f9bba5f2709cba08950071f819d7057aec3a4a2d9ef44cf9cbfb8014d02573c6884a04cff175895823aaef809ebdb034
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-progress@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-progress@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/bed5349682a75db02d362c07ac99fefddbbdc0152c4d5035719498223b9d490ebd834e2d9f64d498424048eb3da7eb7e55ba696e202cd0a048d6e319390e69d3
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-radio-group@npm:1.3.8":
+ version: 1.3.8
+ resolution: "@radix-ui/react-radio-group@npm:1.3.8"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/23af8e8b833da1fc4aa4e67c3607dedee4fc5b39278d2e2b820bec7f7b3c0891b006a8a35c57ba436ddf18735bbd8dad9a598d14632a328753a875fde447975c
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-roving-focus@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-roving-focus@npm:1.1.11"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/2cd43339c36e89a3bf1db8aab34b939113dfbde56bf3a33df2d74757c78c9489b847b1962f1e2441c67e41817d120cb6177943e0f655f47bc1ff8e44fd55b1a2
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-scroll-area@npm:1.2.10":
+ version: 1.2.10
+ resolution: "@radix-ui/react-scroll-area@npm:1.2.10"
+ dependencies:
+ "@radix-ui/number": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/8acdacd255fdfcefe4f72028a13dc554df73327db94c250f54a85b9608aa0313284dbb6c417f28a48e7b7b7bcaa76ddf3297e91ba07d833604d428f869259622
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-select@npm:2.2.6":
+ version: 2.2.6
+ resolution: "@radix-ui/react-select@npm:2.2.6"
+ dependencies:
+ "@radix-ui/number": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/34b2492589c3a4b118a03900d622640033630f30ac93c4a69b3701513117607f4ac3a0d9dd3cad39caa8b6495660f71f3aa9d0074d4eb4dac6804dc0b8408deb
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-separator@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-separator@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/32c0eb4fe018397efbe580542e6e33fdc09b76b96395b2bb4c55da7b6d49224b18f46143bdaf9eb6cb01e166c459fb77508a81d20a591a9034949acee5d171d9
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-slider@npm:1.3.6":
+ version: 1.3.6
+ resolution: "@radix-ui/react-slider@npm:1.3.6"
+ dependencies:
+ "@radix-ui/number": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/a53d7854e28c5ef3d29b76c8d04cc3c723b982b643152cd5a8fefc7a8359180f8fd21753e5a08302a290bc837e7be04f2efad9d308b7a4a23326df6a6b1ac882
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-slot@npm:1.2.3":
+ version: 1.2.3
+ resolution: "@radix-ui/react-slot@npm:1.2.3"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/5913aa0d760f505905779515e4b1f0f71a422350f077cc8d26d1aafe53c97f177fec0e6d7fbbb50d8b5e498aa9df9f707ca75ae3801540c283b26b0136138eef
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-switch@npm:1.2.6":
+ version: 1.2.6
+ resolution: "@radix-ui/react-switch@npm:1.2.6"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/888303cbeb0e69ebba5676b225f9ea0f00f61453c6b8a6b66384b5c5c4c7fb0ccc53493c1eb14ec6d436e5b867b302aadd6af51a1f2e6c04581c583fd9be65be
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-tabs@npm:1.1.13":
+ version: 1.1.13
+ resolution: "@radix-ui/react-tabs@npm:1.1.13"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/a3c78cd8c30dcb95faf1605a8424a1a71dab121dfa6e9c0019bb30d0f36d882762c925b17596d4977990005a255d8ddc0b7454e4f83337fe557b45570a2d8058
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-toast@npm:1.2.15":
+ version: 1.2.15
+ resolution: "@radix-ui/react-toast@npm:1.2.15"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/e7050d356719f438e087e8033e47a8854fba001cf71eb4e0c0203d53a4fbbd35aca9f17f73abe4dadc406d2d85badf4e37065865d07835763d0e3cb9d95a518d
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-toggle-group@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-toggle-group@npm:1.1.11"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-toggle": "npm:1.1.10"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/c8cbccda3e25754ed9f3145c67792df2d5d0ee1a910bde6dc07c4577ab508d4b939f145569d4e2af5b17dc4a5c701473380d8695248f8620cf0a372c05b8e958
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-toggle@npm:1.1.10":
+ version: 1.1.10
+ resolution: "@radix-ui/react-toggle@npm:1.1.10"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/5406cdf5dd7299ae6cfdb4865dc5fd43ca3c475ebcd4e86830bd296d734255b61f749c9bde452ebfaad126033f92dd1112ee9d95982344ffad34491238dcc9b1
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-toolbar@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-toolbar@npm:1.1.11"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-separator": "npm:1.1.7"
+ "@radix-ui/react-toggle-group": "npm:1.1.11"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/3e2ef134608afd08b7a4fd8b222f7638f6d1760f0c3551a9532c91c82b02192b37684c48d5f2bc6c29e3c4be3d3c63ba9a5660d6e99fefa08044c5ded22ee311
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-tooltip@npm:1.2.8":
+ version: 1.2.8
+ resolution: "@radix-ui/react-tooltip@npm:1.2.8"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/de0cbae9c571a00671f160928d819e59502f59be8749f536ab4b180181d9d70aee3925a5b2555f8f32d0bea622bc35f65b70ca7ff0449e4844f891302310cc48
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-callback-ref@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/5f6aff8592dea6a7e46589808912aba3fb3b626cf6edd2b14f01638b61dbbe49eeb9f67cd5601f4c15b2fb547b9a7e825f7c4961acd4dd70176c969ae405f8d8
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-controllable-state@npm:1.2.2":
+ version: 1.2.2
+ resolution: "@radix-ui/react-use-controllable-state@npm:1.2.2"
+ dependencies:
+ "@radix-ui/react-use-effect-event": "npm:0.0.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/f55c4b06e895293aed4b44c9ef26fb24432539f5346fcd6519c7745800535b571058685314e83486a45bf61dc83887e24826490d3068acc317fb0a9010516e63
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-effect-event@npm:0.0.2":
+ version: 0.0.2
+ resolution: "@radix-ui/react-use-effect-event@npm:0.0.2"
+ dependencies:
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/e84ff72a3e76c5ae9c94941028bb4b6472f17d4104481b9eab773deab3da640ecea035e54da9d6f4df8d84c18ef6913baf92b7511bee06930dc58bd0c0add417
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-escape-keydown@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-use-escape-keydown@npm:1.1.1"
+ dependencies:
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/bff53be99e940fef1d3c4df7d560e1d9133182e5a98336255d3063327d1d3dd4ec54a95dc5afe15cca4fb6c184f0a956c70de2815578c318cf995a7f9beabaa1
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-is-hydrated@npm:0.1.0":
+ version: 0.1.0
+ resolution: "@radix-ui/react-use-is-hydrated@npm:0.1.0"
+ dependencies:
+ use-sync-external-store: "npm:^1.5.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/635079bafe32829fc7405895154568ea94a22689b170489fd6d77668e4885e72ff71ed6d0ea3d602852841ef0f1927aa400fee2178d5dfbeb8bc9297da7d6498
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-layout-effect@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/9f98fdaba008dfc58050de60a77670b885792df473cf82c1cef8daee919a5dd5a77d270209f5f0b0abfaac78cb1627396e3ff56c81b735be550409426fe8b040
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-previous@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-use-previous@npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/52f1089d941491cd59b7f52a5679a14e9381711419a0557ce0f3bc9a4c117078224efec54dcced41a3653a13a386a7b6ec75435d61a273e8b9f5d00235f2b182
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-rect@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-use-rect@npm:1.1.1"
+ dependencies:
+ "@radix-ui/rect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/271711404c05c589c8dbdaa748749e7daf44bcc6bffc9ecd910821c3ebca0ee245616cf5b39653ce690f53f875c3836fd3f36f51ab1c628273b6db599eee4864
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-use-size@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-use-size@npm:1.1.1"
+ dependencies:
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/851d09a816f44282e0e9e2147b1b571410174cc048703a50c4fa54d672de994fd1dfff1da9d480ecfd12c77ae8f48d74f01adaf668f074156b8cd0043c6c21d8
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-visually-hidden@npm:1.2.3":
+ version: 1.2.3
+ resolution: "@radix-ui/react-visually-hidden@npm:1.2.3"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/cf86a37f1cbee50a964056f3dc4f6bb1ee79c76daa321f913aa20ff3e1ccdfafbf2b114d7bb616aeefc7c4b895e6ca898523fdb67710d89bd5d8edb739a0d9b6
+ languageName: node
+ linkType: hard
+
+"@radix-ui/rect@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/rect@npm:1.1.1"
+ checksum: 10c0/0dac4f0f15691199abe6a0e067821ddd9d0349c0c05f39834e4eafc8403caf724106884035ae91bbc826e10367e6a5672e7bec4d4243860fa7649de246b1f60b
+ languageName: node
+ linkType: hard
+
+"@react-aria/ssr@npm:^3.5.0":
+ version: 3.9.10
+ resolution: "@react-aria/ssr@npm:3.9.10"
+ dependencies:
+ "@swc/helpers": "npm:^0.5.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+ checksum: 10c0/44acb4c441d9c5d65aab94aa81fd8368413cf2958ab458582296dd78f6ba4783583f2311fa986120060e5c26b54b1f01e8910ffd17e4f41ccc5fc8c357d84089
+ languageName: node
+ linkType: hard
+
+"@redux-saga/core@npm:^1.0.0":
version: 1.2.3
resolution: "@redux-saga/core@npm:1.2.3"
dependencies:
@@ -4136,6 +5929,13 @@ __metadata:
languageName: node
linkType: hard
+"@sec-ant/readable-stream@npm:^0.4.1":
+ version: 0.4.1
+ resolution: "@sec-ant/readable-stream@npm:0.4.1"
+ checksum: 10c0/64e9e9cf161e848067a5bf60cdc04d18495dc28bb63a8d9f8993e4dd99b91ad34e4b563c85de17d91ffb177ec17a0664991d2e115f6543e73236a906068987af
+ languageName: node
+ linkType: hard
+
"@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.6.0":
version: 4.6.0
resolution: "@sindresorhus/is@npm:4.6.0"
@@ -4143,6 +5943,13 @@ __metadata:
languageName: node
linkType: hard
+"@sindresorhus/merge-streams@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "@sindresorhus/merge-streams@npm:4.0.0"
+ checksum: 10c0/482ee543629aa1933b332f811a1ae805a213681ecdd98c042b1c1b89387df63e7812248bb4df3910b02b3cc5589d3d73e4393f30e197c9dde18046ccd471fc6b
+ languageName: node
+ linkType: hard
+
"@sinonjs/commons@npm:^1, @sinonjs/commons@npm:^1.3.0, @sinonjs/commons@npm:^1.7.0":
version: 1.8.6
resolution: "@sinonjs/commons@npm:1.8.6"
@@ -4268,85 +6075,249 @@ __metadata:
languageName: node
linkType: hard
-"@svgr/babel-preset@npm:8.1.0":
- version: 8.1.0
- resolution: "@svgr/babel-preset@npm:8.1.0"
- dependencies:
- "@svgr/babel-plugin-add-jsx-attribute": "npm:8.0.0"
- "@svgr/babel-plugin-remove-jsx-attribute": "npm:8.0.0"
- "@svgr/babel-plugin-remove-jsx-empty-expression": "npm:8.0.0"
- "@svgr/babel-plugin-replace-jsx-attribute-value": "npm:8.0.0"
- "@svgr/babel-plugin-svg-dynamic-title": "npm:8.0.0"
- "@svgr/babel-plugin-svg-em-dimensions": "npm:8.0.0"
- "@svgr/babel-plugin-transform-react-native-svg": "npm:8.1.0"
- "@svgr/babel-plugin-transform-svg-component": "npm:8.0.0"
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: 10c0/49367d3ad0831f79b1056871b91766246f449d4d1168623af5e283fbaefce4a01d77ab00de6b045b55e956f9aae27895823198493cd232d88d3435ea4517ffc5
+"@svgr/babel-preset@npm:8.1.0":
+ version: 8.1.0
+ resolution: "@svgr/babel-preset@npm:8.1.0"
+ dependencies:
+ "@svgr/babel-plugin-add-jsx-attribute": "npm:8.0.0"
+ "@svgr/babel-plugin-remove-jsx-attribute": "npm:8.0.0"
+ "@svgr/babel-plugin-remove-jsx-empty-expression": "npm:8.0.0"
+ "@svgr/babel-plugin-replace-jsx-attribute-value": "npm:8.0.0"
+ "@svgr/babel-plugin-svg-dynamic-title": "npm:8.0.0"
+ "@svgr/babel-plugin-svg-em-dimensions": "npm:8.0.0"
+ "@svgr/babel-plugin-transform-react-native-svg": "npm:8.1.0"
+ "@svgr/babel-plugin-transform-svg-component": "npm:8.0.0"
+ peerDependencies:
+ "@babel/core": ^7.0.0-0
+ checksum: 10c0/49367d3ad0831f79b1056871b91766246f449d4d1168623af5e283fbaefce4a01d77ab00de6b045b55e956f9aae27895823198493cd232d88d3435ea4517ffc5
+ languageName: node
+ linkType: hard
+
+"@svgr/core@npm:^8.1.0":
+ version: 8.1.0
+ resolution: "@svgr/core@npm:8.1.0"
+ dependencies:
+ "@babel/core": "npm:^7.21.3"
+ "@svgr/babel-preset": "npm:8.1.0"
+ camelcase: "npm:^6.2.0"
+ cosmiconfig: "npm:^8.1.3"
+ snake-case: "npm:^3.0.4"
+ checksum: 10c0/6a2f6b1bc79bce39f66f088d468985d518005fc5147ebf4f108570a933818b5951c2cb7da230ddff4b7c8028b5a672b2d33aa2acce012b8b9770073aa5a2d041
+ languageName: node
+ linkType: hard
+
+"@svgr/hast-util-to-babel-ast@npm:8.0.0":
+ version: 8.0.0
+ resolution: "@svgr/hast-util-to-babel-ast@npm:8.0.0"
+ dependencies:
+ "@babel/types": "npm:^7.21.3"
+ entities: "npm:^4.4.0"
+ checksum: 10c0/f4165b583ba9eaf6719e598977a7b3ed182f177983e55f9eb55a6a73982d81277510e9eb7ab41f255151fb9ed4edd11ac4bef95dd872f04ed64966d8c85e0f79
+ languageName: node
+ linkType: hard
+
+"@svgr/plugin-jsx@npm:^8.1.0":
+ version: 8.1.0
+ resolution: "@svgr/plugin-jsx@npm:8.1.0"
+ dependencies:
+ "@babel/core": "npm:^7.21.3"
+ "@svgr/babel-preset": "npm:8.1.0"
+ "@svgr/hast-util-to-babel-ast": "npm:8.0.0"
+ svg-parser: "npm:^2.0.4"
+ peerDependencies:
+ "@svgr/core": "*"
+ checksum: 10c0/07b4d9e00de795540bf70556fa2cc258774d01e97a12a26234c6fdf42b309beb7c10f31ee24d1a71137239347b1547b8bb5587d3a6de10669f95dcfe99cddc56
+ languageName: node
+ linkType: hard
+
+"@swc/helpers@npm:^0.5.0":
+ version: 0.5.17
+ resolution: "@swc/helpers@npm:0.5.17"
+ dependencies:
+ tslib: "npm:^2.8.0"
+ checksum: 10c0/fe1f33ebb968558c5a0c595e54f2e479e4609bff844f9ca9a2d1ffd8dd8504c26f862a11b031f48f75c95b0381c2966c3dd156e25942f90089badd24341e7dbb
+ languageName: node
+ linkType: hard
+
+"@szmarczak/http-timer@npm:^4.0.5":
+ version: 4.0.6
+ resolution: "@szmarczak/http-timer@npm:4.0.6"
+ dependencies:
+ defer-to-connect: "npm:^2.0.0"
+ checksum: 10c0/73946918c025339db68b09abd91fa3001e87fc749c619d2e9c2003a663039d4c3cb89836c98a96598b3d47dec2481284ba85355392644911f5ecd2336536697f
+ languageName: node
+ linkType: hard
+
+"@szmarczak/http-timer@npm:^5.0.1":
+ version: 5.0.1
+ resolution: "@szmarczak/http-timer@npm:5.0.1"
+ dependencies:
+ defer-to-connect: "npm:^2.0.1"
+ checksum: 10c0/4629d2fbb2ea67c2e9dc03af235c0991c79ebdddcbc19aed5d5732fb29ce01c13331e9b1a491584b9069bd6ecde6581dcbf871f11b7eefdebbab34de6cf2197e
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/node@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/node@npm:4.2.1"
+ dependencies:
+ "@jridgewell/remapping": "npm:^2.3.5"
+ enhanced-resolve: "npm:^5.19.0"
+ jiti: "npm:^2.6.1"
+ lightningcss: "npm:1.31.1"
+ magic-string: "npm:^0.30.21"
+ source-map-js: "npm:^1.2.1"
+ tailwindcss: "npm:4.2.1"
+ checksum: 10c0/7b1bf77d2d714df98201cc2f308bee8edfebaf2ef520ec15cb9515e2aadabb28b4d2ecb165dd278716b3f6767da10e4d9a445de34ee8f7ec056fc9c1d8e275a1
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/oxide-android-arm64@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-android-arm64@npm:4.2.1"
+ conditions: os=android & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/oxide-darwin-arm64@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-darwin-arm64@npm:4.2.1"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/oxide-darwin-x64@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-darwin-x64@npm:4.2.1"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/oxide-freebsd-x64@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-freebsd-x64@npm:4.2.1"
+ conditions: os=freebsd & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.2.1"
+ conditions: os=linux & cpu=arm
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/oxide-linux-arm64-gnu@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-linux-arm64-gnu@npm:4.2.1"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/oxide-linux-arm64-musl@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-linux-arm64-musl@npm:4.2.1"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@tailwindcss/oxide-linux-x64-gnu@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-linux-x64-gnu@npm:4.2.1"
+ conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
-"@svgr/core@npm:^8.1.0":
- version: 8.1.0
- resolution: "@svgr/core@npm:8.1.0"
- dependencies:
- "@babel/core": "npm:^7.21.3"
- "@svgr/babel-preset": "npm:8.1.0"
- camelcase: "npm:^6.2.0"
- cosmiconfig: "npm:^8.1.3"
- snake-case: "npm:^3.0.4"
- checksum: 10c0/6a2f6b1bc79bce39f66f088d468985d518005fc5147ebf4f108570a933818b5951c2cb7da230ddff4b7c8028b5a672b2d33aa2acce012b8b9770073aa5a2d041
+"@tailwindcss/oxide-linux-x64-musl@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-linux-x64-musl@npm:4.2.1"
+ conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
-"@svgr/hast-util-to-babel-ast@npm:8.0.0":
- version: 8.0.0
- resolution: "@svgr/hast-util-to-babel-ast@npm:8.0.0"
+"@tailwindcss/oxide-wasm32-wasi@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-wasm32-wasi@npm:4.2.1"
dependencies:
- "@babel/types": "npm:^7.21.3"
- entities: "npm:^4.4.0"
- checksum: 10c0/f4165b583ba9eaf6719e598977a7b3ed182f177983e55f9eb55a6a73982d81277510e9eb7ab41f255151fb9ed4edd11ac4bef95dd872f04ed64966d8c85e0f79
+ "@emnapi/core": "npm:^1.8.1"
+ "@emnapi/runtime": "npm:^1.8.1"
+ "@emnapi/wasi-threads": "npm:^1.1.0"
+ "@napi-rs/wasm-runtime": "npm:^1.1.1"
+ "@tybys/wasm-util": "npm:^0.10.1"
+ tslib: "npm:^2.8.1"
+ conditions: cpu=wasm32
languageName: node
linkType: hard
-"@svgr/plugin-jsx@npm:^8.1.0":
- version: 8.1.0
- resolution: "@svgr/plugin-jsx@npm:8.1.0"
- dependencies:
- "@babel/core": "npm:^7.21.3"
- "@svgr/babel-preset": "npm:8.1.0"
- "@svgr/hast-util-to-babel-ast": "npm:8.0.0"
- svg-parser: "npm:^2.0.4"
- peerDependencies:
- "@svgr/core": "*"
- checksum: 10c0/07b4d9e00de795540bf70556fa2cc258774d01e97a12a26234c6fdf42b309beb7c10f31ee24d1a71137239347b1547b8bb5587d3a6de10669f95dcfe99cddc56
+"@tailwindcss/oxide-win32-arm64-msvc@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-win32-arm64-msvc@npm:4.2.1"
+ conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
-"@swc/helpers@npm:^0.5.0":
- version: 0.5.17
- resolution: "@swc/helpers@npm:0.5.17"
- dependencies:
- tslib: "npm:^2.8.0"
- checksum: 10c0/fe1f33ebb968558c5a0c595e54f2e479e4609bff844f9ca9a2d1ffd8dd8504c26f862a11b031f48f75c95b0381c2966c3dd156e25942f90089badd24341e7dbb
+"@tailwindcss/oxide-win32-x64-msvc@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide-win32-x64-msvc@npm:4.2.1"
+ conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
-"@szmarczak/http-timer@npm:^4.0.5":
- version: 4.0.6
- resolution: "@szmarczak/http-timer@npm:4.0.6"
- dependencies:
- defer-to-connect: "npm:^2.0.0"
- checksum: 10c0/73946918c025339db68b09abd91fa3001e87fc749c619d2e9c2003a663039d4c3cb89836c98a96598b3d47dec2481284ba85355392644911f5ecd2336536697f
+"@tailwindcss/oxide@npm:4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/oxide@npm:4.2.1"
+ dependencies:
+ "@tailwindcss/oxide-android-arm64": "npm:4.2.1"
+ "@tailwindcss/oxide-darwin-arm64": "npm:4.2.1"
+ "@tailwindcss/oxide-darwin-x64": "npm:4.2.1"
+ "@tailwindcss/oxide-freebsd-x64": "npm:4.2.1"
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "npm:4.2.1"
+ "@tailwindcss/oxide-linux-arm64-gnu": "npm:4.2.1"
+ "@tailwindcss/oxide-linux-arm64-musl": "npm:4.2.1"
+ "@tailwindcss/oxide-linux-x64-gnu": "npm:4.2.1"
+ "@tailwindcss/oxide-linux-x64-musl": "npm:4.2.1"
+ "@tailwindcss/oxide-wasm32-wasi": "npm:4.2.1"
+ "@tailwindcss/oxide-win32-arm64-msvc": "npm:4.2.1"
+ "@tailwindcss/oxide-win32-x64-msvc": "npm:4.2.1"
+ dependenciesMeta:
+ "@tailwindcss/oxide-android-arm64":
+ optional: true
+ "@tailwindcss/oxide-darwin-arm64":
+ optional: true
+ "@tailwindcss/oxide-darwin-x64":
+ optional: true
+ "@tailwindcss/oxide-freebsd-x64":
+ optional: true
+ "@tailwindcss/oxide-linux-arm-gnueabihf":
+ optional: true
+ "@tailwindcss/oxide-linux-arm64-gnu":
+ optional: true
+ "@tailwindcss/oxide-linux-arm64-musl":
+ optional: true
+ "@tailwindcss/oxide-linux-x64-gnu":
+ optional: true
+ "@tailwindcss/oxide-linux-x64-musl":
+ optional: true
+ "@tailwindcss/oxide-wasm32-wasi":
+ optional: true
+ "@tailwindcss/oxide-win32-arm64-msvc":
+ optional: true
+ "@tailwindcss/oxide-win32-x64-msvc":
+ optional: true
+ checksum: 10c0/4f9bfa40cde6925eed1759caf857831779a834a1b8776cc7df5bb48a279b7dcd2e761a31ffbd297d135a64b58f25e092d7c781c06cf44667746f7e5a5a3e0183
languageName: node
linkType: hard
-"@szmarczak/http-timer@npm:^5.0.1":
- version: 5.0.1
- resolution: "@szmarczak/http-timer@npm:5.0.1"
+"@tailwindcss/vite@npm:^4.2.1":
+ version: 4.2.1
+ resolution: "@tailwindcss/vite@npm:4.2.1"
dependencies:
- defer-to-connect: "npm:^2.0.1"
- checksum: 10c0/4629d2fbb2ea67c2e9dc03af235c0991c79ebdddcbc19aed5d5732fb29ce01c13331e9b1a491584b9069bd6ecde6581dcbf871f11b7eefdebbab34de6cf2197e
+ "@tailwindcss/node": "npm:4.2.1"
+ "@tailwindcss/oxide": "npm:4.2.1"
+ tailwindcss: "npm:4.2.1"
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7
+ checksum: 10c0/44ee3bddffec12f3433b9d589e643f8a75edc78171766bc968ce5e95b4228c76b44e80cf9ddd5edb4edc11df658790099a98e23f976f9a38d812ddbeb443b3f9
languageName: node
linkType: hard
@@ -4610,6 +6581,17 @@ __metadata:
languageName: node
linkType: hard
+"@ts-morph/common@npm:~0.27.0":
+ version: 0.27.0
+ resolution: "@ts-morph/common@npm:0.27.0"
+ dependencies:
+ fast-glob: "npm:^3.3.3"
+ minimatch: "npm:^10.0.1"
+ path-browserify: "npm:^1.0.1"
+ checksum: 10c0/3daa267bd78114ff504eb064c5215da6e46589e775b781ec0da4998d999b0d7130eff287e70d6e13e0a0a897ea16e9387f4cd885b4b9d6d628f318cecb81d473
+ languageName: node
+ linkType: hard
+
"@tsconfig/node10@npm:^1.0.7":
version: 1.0.9
resolution: "@tsconfig/node10@npm:1.0.9"
@@ -4638,6 +6620,15 @@ __metadata:
languageName: node
linkType: hard
+"@tybys/wasm-util@npm:^0.10.1":
+ version: 0.10.1
+ resolution: "@tybys/wasm-util@npm:0.10.1"
+ dependencies:
+ tslib: "npm:^2.4.0"
+ checksum: 10c0/b255094f293794c6d2289300c5fbcafbb5532a3aed3a5ffd2f8dc1828e639b88d75f6a376dd8f94347a44813fd7a7149d8463477a9a49525c8b2dcaa38c2d1e8
+ languageName: node
+ linkType: hard
+
"@types/accepts@npm:^1.3.5":
version: 1.3.7
resolution: "@types/accepts@npm:1.3.7"
@@ -5519,6 +7510,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/statuses@npm:^2.0.6":
+ version: 2.0.6
+ resolution: "@types/statuses@npm:2.0.6"
+ checksum: 10c0/dd88c220b0e2c6315686289525fd61472d2204d2e4bef4941acfb76bda01d3066f749ac74782aab5b537a45314fcd7d6261eefa40b6ec872691f5803adaa608d
+ languageName: node
+ linkType: hard
+
"@types/styled-components@npm:^5.1.26":
version: 5.1.26
resolution: "@types/styled-components@npm:5.1.26"
@@ -5562,6 +7560,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/validate-npm-package-name@npm:^4.0.2":
+ version: 4.0.2
+ resolution: "@types/validate-npm-package-name@npm:4.0.2"
+ checksum: 10c0/5a109fe12570b7125740fdac8678970e3caf7de0cbd6e15cf93dad67a427a3a83e0759b5d4b64fd75bce6cdf43b0569275456a35183059046cba6c25d096b916
+ languageName: node
+ linkType: hard
+
"@types/warning@npm:^3.0.3":
version: 3.0.3
resolution: "@types/warning@npm:3.0.3"
@@ -5915,6 +7920,16 @@ __metadata:
languageName: node
linkType: hard
+"accepts@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "accepts@npm:2.0.0"
+ dependencies:
+ mime-types: "npm:^3.0.0"
+ negotiator: "npm:^1.0.0"
+ checksum: 10c0/98374742097e140891546076215f90c32644feacf652db48412329de4c2a529178a81aa500fbb13dd3e6cbf6e68d829037b123ac037fc9a08bcec4b87b358eef
+ languageName: node
+ linkType: hard
+
"acorn-import-attributes@npm:^1.9.5":
version: 1.9.5
resolution: "acorn-import-attributes@npm:1.9.5"
@@ -5999,6 +8014,13 @@ __metadata:
languageName: node
linkType: hard
+"agent-base@npm:^7.1.2":
+ version: 7.1.4
+ resolution: "agent-base@npm:7.1.4"
+ checksum: 10c0/c2c9ab7599692d594b6a161559ada307b7a624fa4c7b03e3afdb5a5e31cd0e53269115b620fcab024c5ac6a6f37fa5eb2e004f076ad30f5f7e6b8b671f7b35fe
+ languageName: node
+ linkType: hard
+
"aggregate-error@npm:^3.0.0":
version: 3.1.0
resolution: "aggregate-error@npm:3.1.0"
@@ -6023,6 +8045,20 @@ __metadata:
languageName: node
linkType: hard
+"ajv-formats@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "ajv-formats@npm:3.0.1"
+ dependencies:
+ ajv: "npm:^8.0.0"
+ peerDependencies:
+ ajv: ^8.0.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+ checksum: 10c0/168d6bca1ea9f163b41c8147bae537e67bd963357a5488a1eaf3abe8baa8eec806d4e45f15b10767e6020679315c7e1e5e6803088dfb84efa2b4e9353b83dd0a
+ languageName: node
+ linkType: hard
+
"ajv@npm:6.10.2":
version: 6.10.2
resolution: "ajv@npm:6.10.2"
@@ -6113,6 +8149,13 @@ __metadata:
languageName: node
linkType: hard
+"ansi-regex@npm:^6.2.2":
+ version: 6.2.2
+ resolution: "ansi-regex@npm:6.2.2"
+ checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f
+ languageName: node
+ linkType: hard
+
"ansi-styles@npm:^3.2.0, ansi-styles@npm:^3.2.1":
version: 3.2.1
resolution: "ansi-styles@npm:3.2.1"
@@ -6330,6 +8373,15 @@ __metadata:
languageName: node
linkType: hard
+"aria-hidden@npm:^1.2.4":
+ version: 1.2.6
+ resolution: "aria-hidden@npm:1.2.6"
+ dependencies:
+ tslib: "npm:^2.0.0"
+ checksum: 10c0/7720cb539497a9f760f68f98a4b30f22c6767aa0e72fa7d58279f7c164e258fc38b2699828f8de881aab0fc8e9c56d1313a3f1a965046fc0381a554dbc72b54a
+ languageName: node
+ linkType: hard
+
"array-buffer-byte-length@npm:^1.0.0":
version: 1.0.0
resolution: "array-buffer-byte-length@npm:1.0.0"
@@ -6446,6 +8498,15 @@ __metadata:
languageName: node
linkType: hard
+"ast-types@npm:^0.16.1":
+ version: 0.16.1
+ resolution: "ast-types@npm:0.16.1"
+ dependencies:
+ tslib: "npm:^2.0.1"
+ checksum: 10c0/abcc49e42eb921a7ebc013d5bec1154651fb6dbc3f497541d488859e681256901b2990b954d530ba0da4d0851271d484f7057d5eff5e07cb73e8b10909f711bf
+ languageName: node
+ linkType: hard
+
"async-eventemitter@npm:0.2.4":
version: 0.2.4
resolution: "async-eventemitter@npm:0.2.4"
@@ -6455,6 +8516,20 @@ __metadata:
languageName: node
linkType: hard
+"async-function@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "async-function@npm:1.0.0"
+ checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73
+ languageName: node
+ linkType: hard
+
+"async-generator-function@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "async-generator-function@npm:1.0.0"
+ checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186
+ languageName: node
+ linkType: hard
+
"async-limiter@npm:~1.0.0":
version: 1.0.1
resolution: "async-limiter@npm:1.0.1"
@@ -6588,6 +8663,13 @@ __metadata:
languageName: node
linkType: hard
+"balanced-match@npm:^4.0.2":
+ version: 4.0.4
+ resolution: "balanced-match@npm:4.0.4"
+ checksum: 10c0/07e86102a3eb2ee2a6a1a89164f29d0dbaebd28f2ca3f5ca786f36b8b23d9e417eb3be45a4acf754f837be5ac0a2317de90d3fcb7f4f4dc95720a1f36b26a17b
+ languageName: node
+ linkType: hard
+
"base-x@npm:^3.0.2, base-x@npm:^3.0.8":
version: 3.0.9
resolution: "base-x@npm:3.0.9"
@@ -6792,6 +8874,23 @@ __metadata:
languageName: node
linkType: hard
+"body-parser@npm:^2.2.1":
+ version: 2.2.2
+ resolution: "body-parser@npm:2.2.2"
+ dependencies:
+ bytes: "npm:^3.1.2"
+ content-type: "npm:^1.0.5"
+ debug: "npm:^4.4.3"
+ http-errors: "npm:^2.0.0"
+ iconv-lite: "npm:^0.7.0"
+ on-finished: "npm:^2.4.1"
+ qs: "npm:^6.14.1"
+ raw-body: "npm:^3.0.1"
+ type-is: "npm:^2.0.1"
+ checksum: 10c0/95a830a003b38654b75166ca765358aa92ee3d561bf0e41d6ccdde0e1a0c9783cab6b90b20eb635d23172c010b59d3563a137a738e74da4ba714463510d05137
+ languageName: node
+ linkType: hard
+
"bootstrap@npm:^5.3":
version: 5.3.8
resolution: "bootstrap@npm:5.3.8"
@@ -6820,6 +8919,15 @@ __metadata:
languageName: node
linkType: hard
+"brace-expansion@npm:^5.0.2":
+ version: 5.0.4
+ resolution: "brace-expansion@npm:5.0.4"
+ dependencies:
+ balanced-match: "npm:^4.0.2"
+ checksum: 10c0/359cbcfa80b2eb914ca1f3440e92313fbfe7919ee6b274c35db55bec555aded69dac5ee78f102cec90c35f98c20fa43d10936d0cd9978158823c249257e1643a
+ languageName: node
+ linkType: hard
+
"braces@npm:^3.0.2, braces@npm:~3.0.2":
version: 3.0.2
resolution: "braces@npm:3.0.2"
@@ -6960,7 +9068,7 @@ __metadata:
languageName: node
linkType: hard
-"browserslist@npm:^4.24.0":
+"browserslist@npm:^4.24.0, browserslist@npm:^4.26.2":
version: 4.28.1
resolution: "browserslist@npm:4.28.1"
dependencies:
@@ -7091,6 +9199,15 @@ __metadata:
languageName: node
linkType: hard
+"bundle-name@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "bundle-name@npm:4.1.0"
+ dependencies:
+ run-applescript: "npm:^7.0.0"
+ checksum: 10c0/8e575981e79c2bcf14d8b1c027a3775c095d362d1382312f444a7c861b0e21513c0bd8db5bd2b16e50ba0709fa622d4eab6b53192d222120305e68359daece29
+ languageName: node
+ linkType: hard
+
"busboy@npm:^1.6.0":
version: 1.6.0
resolution: "busboy@npm:1.6.0"
@@ -7107,7 +9224,7 @@ __metadata:
languageName: node
linkType: hard
-"bytes@npm:3.1.2":
+"bytes@npm:3.1.2, bytes@npm:^3.1.2, bytes@npm:~3.1.2":
version: 3.1.2
resolution: "bytes@npm:3.1.2"
checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e
@@ -7206,7 +9323,7 @@ __metadata:
languageName: node
linkType: hard
-"call-bound@npm:^1.0.3, call-bound@npm:^1.0.4":
+"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3, call-bound@npm:^1.0.4":
version: 1.0.4
resolution: "call-bound@npm:1.0.4"
dependencies:
@@ -7353,6 +9470,13 @@ __metadata:
languageName: node
linkType: hard
+"chalk@npm:^5.3.0":
+ version: 5.6.2
+ resolution: "chalk@npm:5.6.2"
+ checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976
+ languageName: node
+ linkType: hard
+
"change-case@npm:3.0.2":
version: 3.0.2
resolution: "change-case@npm:3.0.2"
@@ -7484,6 +9608,15 @@ __metadata:
languageName: node
linkType: hard
+"class-variance-authority@npm:^0.7.1":
+ version: 0.7.1
+ resolution: "class-variance-authority@npm:0.7.1"
+ dependencies:
+ clsx: "npm:^2.1.1"
+ checksum: 10c0/0f438cea22131808b99272de0fa933c2532d5659773bfec0c583de7b3f038378996d3350683426b8e9c74a6286699382106d71fbec52f0dd5fbb191792cccb5b
+ languageName: node
+ linkType: hard
+
"classnames@npm:^2.2.6":
version: 2.3.2
resolution: "classnames@npm:2.3.2"
@@ -7514,6 +9647,29 @@ __metadata:
languageName: node
linkType: hard
+"cli-cursor@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "cli-cursor@npm:5.0.0"
+ dependencies:
+ restore-cursor: "npm:^5.0.0"
+ checksum: 10c0/7ec62f69b79f6734ab209a3e4dbdc8af7422d44d360a7cb1efa8a0887bbe466a6e625650c466fe4359aee44dbe2dc0b6994b583d40a05d0808a5cb193641d220
+ languageName: node
+ linkType: hard
+
+"cli-spinners@npm:^2.9.2":
+ version: 2.9.2
+ resolution: "cli-spinners@npm:2.9.2"
+ checksum: 10c0/907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3
+ languageName: node
+ linkType: hard
+
+"cli-width@npm:^4.1.0":
+ version: 4.1.0
+ resolution: "cli-width@npm:4.1.0"
+ checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f
+ languageName: node
+ linkType: hard
+
"clipboard@npm:*, clipboard@npm:^2.0.1":
version: 2.0.11
resolution: "clipboard@npm:2.0.11"
@@ -7574,6 +9730,20 @@ __metadata:
languageName: node
linkType: hard
+"clsx@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "clsx@npm:2.1.1"
+ checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839
+ languageName: node
+ linkType: hard
+
+"code-block-writer@npm:^13.0.3":
+ version: 13.0.3
+ resolution: "code-block-writer@npm:13.0.3"
+ checksum: 10c0/87db97b37583f71cfd7eced8bf3f0a0a0ca53af912751a734372b36c08cd27f3e8a4878ec05591c0cd9ae11bea8add1423e132d660edd86aab952656dd41fd66
+ languageName: node
+ linkType: hard
+
"code-point-at@npm:^1.0.0":
version: 1.1.0
resolution: "code-point-at@npm:1.1.0"
@@ -7636,6 +9806,20 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:^11.1.0":
+ version: 11.1.0
+ resolution: "commander@npm:11.1.0"
+ checksum: 10c0/13cc6ac875e48780250f723fb81c1c1178d35c5decb1abb1b628b3177af08a8554e76b2c0f29de72d69eef7c864d12613272a71fabef8047922bc622ab75a179
+ languageName: node
+ linkType: hard
+
+"commander@npm:^14.0.0":
+ version: 14.0.3
+ resolution: "commander@npm:14.0.3"
+ checksum: 10c0/755652564bbf56ff2ff083313912b326450d3f8d8c85f4b71416539c9a05c3c67dbd206821ca72635bf6b160e2afdefcb458e86b317827d5cb333b69ce7f1a24
+ languageName: node
+ linkType: hard
+
"commander@npm:^2.12.2, commander@npm:^2.20.3":
version: 2.20.3
resolution: "commander@npm:2.20.3"
@@ -7732,6 +9916,13 @@ __metadata:
languageName: node
linkType: hard
+"content-disposition@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "content-disposition@npm:1.0.1"
+ checksum: 10c0/bd7ff1fe8d2542d3a2b9a29428cc3591f6ac27bb5595bba2c69664408a68f9538b14cbd92479796ea835b317a09a527c8c7209c4200381dedb0c34d3b658849e
+ languageName: node
+ linkType: hard
+
"content-hash@npm:^2.5.2":
version: 2.5.2
resolution: "content-hash@npm:2.5.2"
@@ -7743,7 +9934,7 @@ __metadata:
languageName: node
linkType: hard
-"content-type@npm:~1.0.4, content-type@npm:~1.0.5":
+"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5":
version: 1.0.5
resolution: "content-type@npm:1.0.5"
checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af
@@ -7775,6 +9966,13 @@ __metadata:
languageName: node
linkType: hard
+"cookie-signature@npm:^1.2.1":
+ version: 1.2.2
+ resolution: "cookie-signature@npm:1.2.2"
+ checksum: 10c0/54e05df1a293b3ce81589b27dddc445f462f6fa6812147c033350cd3561a42bc14481674e05ed14c7bd0ce1e8bb3dc0e40851bad75415733711294ddce0b7bc6
+ languageName: node
+ linkType: hard
+
"cookie@npm:0.5.0":
version: 0.5.0
resolution: "cookie@npm:0.5.0"
@@ -7782,6 +9980,20 @@ __metadata:
languageName: node
linkType: hard
+"cookie@npm:^0.7.1":
+ version: 0.7.2
+ resolution: "cookie@npm:0.7.2"
+ checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2
+ languageName: node
+ linkType: hard
+
+"cookie@npm:^1.0.2":
+ version: 1.1.1
+ resolution: "cookie@npm:1.1.1"
+ checksum: 10c0/79c4ddc0fcad9c4f045f826f42edf54bcc921a29586a4558b0898277fa89fb47be95bc384c2253f493af7b29500c830da28341274527328f18eba9f58afa112c
+ languageName: node
+ linkType: hard
+
"cookie@npm:~0.4.1":
version: 0.4.2
resolution: "cookie@npm:0.4.2"
@@ -7860,6 +10072,23 @@ __metadata:
languageName: node
linkType: hard
+"cosmiconfig@npm:^9.0.0":
+ version: 9.0.1
+ resolution: "cosmiconfig@npm:9.0.1"
+ dependencies:
+ env-paths: "npm:^2.2.1"
+ import-fresh: "npm:^3.3.0"
+ js-yaml: "npm:^4.1.0"
+ parse-json: "npm:^5.2.0"
+ peerDependencies:
+ typescript: ">=4.9.5"
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ checksum: 10c0/a5d4d95599687532ee072bca60170133c24d4e08cd795529e0f22c6ce5fde9409eaf4f26e36e3d671f43270ef858fc68f3c7b0ec28e58fac7ddebda5b7725306
+ languageName: node
+ linkType: hard
+
"cpu-features@npm:~0.0.8":
version: 0.0.8
resolution: "cpu-features@npm:0.0.8"
@@ -7956,7 +10185,7 @@ __metadata:
languageName: node
linkType: hard
-"cross-spawn@npm:^7.0.6":
+"cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
dependencies:
@@ -8019,6 +10248,15 @@ __metadata:
languageName: node
linkType: hard
+"cssesc@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "cssesc@npm:3.0.0"
+ bin:
+ cssesc: bin/cssesc
+ checksum: 10c0/6bcfd898662671be15ae7827120472c5667afb3d7429f1f917737f3bf84c4176003228131b643ae74543f17a394446247df090c597bb9a728cce298606ed0aa7
+ languageName: node
+ linkType: hard
+
"cssfilter@npm:0.0.10":
version: 0.0.10
resolution: "cssfilter@npm:0.0.10"
@@ -8160,6 +10398,18 @@ __metadata:
languageName: node
linkType: hard
+"debug@npm:^4.4.0, debug@npm:^4.4.3":
+ version: 4.4.3
+ resolution: "debug@npm:4.4.3"
+ dependencies:
+ ms: "npm:^2.1.3"
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6
+ languageName: node
+ linkType: hard
+
"decamelize@npm:^1.1.1":
version: 1.2.0
resolution: "decamelize@npm:1.2.0"
@@ -8208,6 +10458,18 @@ __metadata:
languageName: node
linkType: hard
+"dedent@npm:^1.6.0":
+ version: 1.7.2
+ resolution: "dedent@npm:1.7.2"
+ peerDependencies:
+ babel-plugin-macros: ^3.1.0
+ peerDependenciesMeta:
+ babel-plugin-macros:
+ optional: true
+ checksum: 10c0/acaff07cac355b93f17b1b17ebbb84d3cc55af6ab4b7814c3f505e061903e168bc6bf9ddce331552d64dee1525f0b4c549c9ade46aebfac6f69caaed74e90751
+ languageName: node
+ linkType: hard
+
"deep-eql@npm:^4.1.3":
version: 4.1.3
resolution: "deep-eql@npm:4.1.3"
@@ -8231,6 +10493,30 @@ __metadata:
languageName: node
linkType: hard
+"deepmerge@npm:^4.3.1":
+ version: 4.3.1
+ resolution: "deepmerge@npm:4.3.1"
+ checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044
+ languageName: node
+ linkType: hard
+
+"default-browser-id@npm:^5.0.0":
+ version: 5.0.1
+ resolution: "default-browser-id@npm:5.0.1"
+ checksum: 10c0/5288b3094c740ef3a86df9b999b04ff5ba4dee6b64e7b355c0fff5217752c8c86908d67f32f6cba9bb4f9b7b61a1b640c0a4f9e34c57e0ff3493559a625245ee
+ languageName: node
+ linkType: hard
+
+"default-browser@npm:^5.4.0":
+ version: 5.5.0
+ resolution: "default-browser@npm:5.5.0"
+ dependencies:
+ bundle-name: "npm:^4.1.0"
+ default-browser-id: "npm:^5.0.0"
+ checksum: 10c0/576593b617b17a7223014b4571bfe1c06a2581a4eb8b130985d90d253afa3f40999caec70eb0e5776e80d4af6a41cce91018cd3f86e57ad578bf59e46fb19abe
+ languageName: node
+ linkType: hard
+
"defer-to-connect@npm:^2.0.0, defer-to-connect@npm:^2.0.1":
version: 2.0.1
resolution: "defer-to-connect@npm:2.0.1"
@@ -8270,6 +10556,13 @@ __metadata:
languageName: node
linkType: hard
+"define-lazy-prop@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "define-lazy-prop@npm:3.0.0"
+ checksum: 10c0/5ab0b2bf3fa58b3a443140bbd4cd3db1f91b985cc8a246d330b9ac3fc0b6a325a6d82bddc0b055123d745b3f9931afeea74a5ec545439a1630b9c8512b0eeb49
+ languageName: node
+ linkType: hard
+
"define-properties@npm:^1.1.3, define-properties@npm:^1.1.4, define-properties@npm:^1.2.0":
version: 1.2.0
resolution: "define-properties@npm:1.2.0"
@@ -8334,7 +10627,7 @@ __metadata:
languageName: node
linkType: hard
-"depd@npm:2.0.0, depd@npm:~2.0.0":
+"depd@npm:2.0.0, depd@npm:^2.0.0, depd@npm:~2.0.0":
version: 2.0.0
resolution: "depd@npm:2.0.0"
checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c
@@ -8379,6 +10672,20 @@ __metadata:
languageName: node
linkType: hard
+"detect-libc@npm:^2.0.3":
+ version: 2.1.2
+ resolution: "detect-libc@npm:2.1.2"
+ checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
+ languageName: node
+ linkType: hard
+
+"detect-node-es@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "detect-node-es@npm:1.1.0"
+ checksum: 10c0/e562f00de23f10c27d7119e1af0e7388407eb4b06596a25f6d79a360094a109ff285de317f02b090faae093d314cf6e73ac3214f8a5bb3a0def5bece94557fbe
+ languageName: node
+ linkType: hard
+
"diff-sequences@npm:^24.9.0":
version: 24.9.0
resolution: "diff-sequences@npm:24.9.0"
@@ -8421,6 +10728,13 @@ __metadata:
languageName: node
linkType: hard
+"diff@npm:^8.0.2":
+ version: 8.0.3
+ resolution: "diff@npm:8.0.3"
+ checksum: 10c0/d29321c70d3545fdcb56c5fdd76028c3f04c012462779e062303d4c3c531af80d2c360c26b871e6e2b9a971d2422d47e1779a859106c4cac4b5d2d143df70e20
+ languageName: node
+ linkType: hard
+
"diffie-hellman@npm:^5.0.0":
version: 5.0.3
resolution: "diffie-hellman@npm:5.0.3"
@@ -8547,6 +10861,13 @@ __metadata:
languageName: node
linkType: hard
+"dotenv@npm:^17.2.1":
+ version: 17.3.1
+ resolution: "dotenv@npm:17.3.1"
+ checksum: 10c0/c78e0c2d5a549c751e544cc60e2b95e7cb67e0c551f42e094d161c6b297aa44b630a3c2dcacf5569e529a6c2a6b84e2ab9be8d37b299d425df5a18b81ce4a35f
+ languageName: node
+ linkType: hard
+
"dotenv@npm:^8.2.0":
version: 8.6.0
resolution: "dotenv@npm:8.6.0"
@@ -8589,6 +10910,18 @@ __metadata:
languageName: node
linkType: hard
+"eciesjs@npm:^0.4.10":
+ version: 0.4.18
+ resolution: "eciesjs@npm:0.4.18"
+ dependencies:
+ "@ecies/ciphers": "npm:^0.2.5"
+ "@noble/ciphers": "npm:^1.3.0"
+ "@noble/curves": "npm:^1.9.7"
+ "@noble/hashes": "npm:^1.8.0"
+ checksum: 10c0/ecccb17b2fd33c143cf9b78014cca4cb00db1615ef8bd20fb6c835b62ef744ff772dd1545aff2cc215a2924bd9cadb2abd8c06a408e85c78b91efcb1712bc1eb
+ languageName: node
+ linkType: hard
+
"ee-first@npm:1.1.1":
version: 1.1.1
resolution: "ee-first@npm:1.1.1"
@@ -8634,6 +10967,34 @@ __metadata:
languageName: node
linkType: hard
+"embla-carousel-react@npm:^8.6.0":
+ version: 8.6.0
+ resolution: "embla-carousel-react@npm:8.6.0"
+ dependencies:
+ embla-carousel: "npm:8.6.0"
+ embla-carousel-reactive-utils: "npm:8.6.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ checksum: 10c0/461b0c427a6fca62e730c7b39d08e9c55b01a2138c173d5b9ebf77ec92092a900b45ac384731c77382b48d5d65946994d3abba6e9899a66d6e16f8ddf191ed1c
+ languageName: node
+ linkType: hard
+
+"embla-carousel-reactive-utils@npm:8.6.0":
+ version: 8.6.0
+ resolution: "embla-carousel-reactive-utils@npm:8.6.0"
+ peerDependencies:
+ embla-carousel: 8.6.0
+ checksum: 10c0/33d07e6df4d8dec9b4fc95858838ea2d5e5df078039c05b2a342c05ba4979d8d18564a1c607da445fed744daf969a0b5a90124ce487a854e86ced6f0458b51e9
+ languageName: node
+ linkType: hard
+
+"embla-carousel@npm:8.6.0":
+ version: 8.6.0
+ resolution: "embla-carousel@npm:8.6.0"
+ checksum: 10c0/f4c598e7be28b70340d31ffd2bebb2472db370b0c81d9b089bf9555cf618695f35dc4a0694565c994c9ab972731123063f945aa09ff485df0df761d79c6a08ef
+ languageName: node
+ linkType: hard
+
"emittery@npm:0.10.0":
version: 0.10.0
resolution: "emittery@npm:0.10.0"
@@ -8648,6 +11009,13 @@ __metadata:
languageName: node
linkType: hard
+"emoji-regex@npm:^10.3.0":
+ version: 10.6.0
+ resolution: "emoji-regex@npm:10.6.0"
+ checksum: 10c0/1e4aa097bb007301c3b4b1913879ae27327fdc48e93eeefefe3b87e495eb33c5af155300be951b4349ff6ac084f4403dc9eff970acba7c1c572d89396a9a32d7
+ languageName: node
+ linkType: hard
+
"emoji-regex@npm:^8.0.0":
version: 8.0.0
resolution: "emoji-regex@npm:8.0.0"
@@ -8662,6 +11030,13 @@ __metadata:
languageName: node
linkType: hard
+"encodeurl@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "encodeurl@npm:2.0.0"
+ checksum: 10c0/5d317306acb13e6590e28e27924c754163946a2480de11865c991a3a7eed4315cd3fba378b543ca145829569eefe9b899f3d84bb09870f675ae60bc924b01ceb
+ languageName: node
+ linkType: hard
+
"encodeurl@npm:~1.0.2":
version: 1.0.2
resolution: "encodeurl@npm:1.0.2"
@@ -8756,6 +11131,16 @@ __metadata:
languageName: node
linkType: hard
+"enhanced-resolve@npm:^5.19.0":
+ version: 5.20.0
+ resolution: "enhanced-resolve@npm:5.20.0"
+ dependencies:
+ graceful-fs: "npm:^4.2.4"
+ tapable: "npm:^2.3.0"
+ checksum: 10c0/4ed5f38406fc9ad74c58a3d63b8215862243ab0ed6b0efc51ccdb72cdcedd3ac8638abe298680b279d7a83c3cb140e5eea7a5f8bd99696c74588f07ad89a95a7
+ languageName: node
+ linkType: hard
+
"entities@npm:^4.4.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
@@ -9149,7 +11534,7 @@ __metadata:
languageName: node
linkType: hard
-"escape-html@npm:~1.0.3":
+"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3":
version: 1.0.3
resolution: "escape-html@npm:1.0.3"
checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3
@@ -9280,7 +11665,7 @@ __metadata:
languageName: node
linkType: hard
-"esprima@npm:^4.0.0":
+"esprima@npm:^4.0.0, esprima@npm:~4.0.0":
version: 4.0.1
resolution: "esprima@npm:4.0.1"
bin:
@@ -9329,7 +11714,7 @@ __metadata:
languageName: node
linkType: hard
-"etag@npm:~1.8.1":
+"etag@npm:^1.8.1, etag@npm:~1.8.1":
version: 1.8.1
resolution: "etag@npm:1.8.1"
checksum: 10c0/12be11ef62fb9817314d790089a0a49fae4e1b50594135dcb8076312b7d7e470884b5100d249b28c18581b7fd52f8b485689ffae22a11ed9ec17377a33a08f84
@@ -9569,6 +11954,22 @@ __metadata:
languageName: node
linkType: hard
+"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.1":
+ version: 3.0.6
+ resolution: "eventsource-parser@npm:3.0.6"
+ checksum: 10c0/70b8ccec7dac767ef2eca43f355e0979e70415701691382a042a2df8d6a68da6c2fca35363669821f3da876d29c02abe9b232964637c1b6635c940df05ada78a
+ languageName: node
+ linkType: hard
+
+"eventsource@npm:^3.0.2":
+ version: 3.0.7
+ resolution: "eventsource@npm:3.0.7"
+ dependencies:
+ eventsource-parser: "npm:^3.0.1"
+ checksum: 10c0/c48a73c38f300e33e9f11375d4ee969f25cbb0519608a12378a38068055ae8b55b6e0e8a49c3f91c784068434efe1d9f01eb49b6315b04b0da9157879ce2f67d
+ languageName: node
+ linkType: hard
+
"evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3":
version: 1.0.3
resolution: "evp_bytestokey@npm:1.0.3"
@@ -9580,6 +11981,43 @@ __metadata:
languageName: node
linkType: hard
+"execa@npm:^5.1.1":
+ version: 5.1.1
+ resolution: "execa@npm:5.1.1"
+ dependencies:
+ cross-spawn: "npm:^7.0.3"
+ get-stream: "npm:^6.0.0"
+ human-signals: "npm:^2.1.0"
+ is-stream: "npm:^2.0.0"
+ merge-stream: "npm:^2.0.0"
+ npm-run-path: "npm:^4.0.1"
+ onetime: "npm:^5.1.2"
+ signal-exit: "npm:^3.0.3"
+ strip-final-newline: "npm:^2.0.0"
+ checksum: 10c0/c8e615235e8de4c5addf2fa4c3da3e3aa59ce975a3e83533b4f6a71750fb816a2e79610dc5f1799b6e28976c9ae86747a36a606655bf8cb414a74d8d507b304f
+ languageName: node
+ linkType: hard
+
+"execa@npm:^9.6.0":
+ version: 9.6.1
+ resolution: "execa@npm:9.6.1"
+ dependencies:
+ "@sindresorhus/merge-streams": "npm:^4.0.0"
+ cross-spawn: "npm:^7.0.6"
+ figures: "npm:^6.1.0"
+ get-stream: "npm:^9.0.0"
+ human-signals: "npm:^8.0.1"
+ is-plain-obj: "npm:^4.1.0"
+ is-stream: "npm:^4.0.1"
+ npm-run-path: "npm:^6.0.0"
+ pretty-ms: "npm:^9.2.0"
+ signal-exit: "npm:^4.1.0"
+ strip-final-newline: "npm:^4.0.0"
+ yoctocolors: "npm:^2.1.1"
+ checksum: 10c0/636b36585306a3c8bc3a9d7b25d2d915fb06d8c9b9b02a804280d62562de3b34535affc1b7702b039320e0953daa6545a073f3c4b63fe974c1fe11336c56b467
+ languageName: node
+ linkType: hard
+
"expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2":
version: 2.0.2
resolution: "expand-tilde@npm:2.0.2"
@@ -9614,6 +12052,17 @@ __metadata:
languageName: node
linkType: hard
+"express-rate-limit@npm:^8.2.1":
+ version: 8.3.1
+ resolution: "express-rate-limit@npm:8.3.1"
+ dependencies:
+ ip-address: "npm:10.1.0"
+ peerDependencies:
+ express: ">= 4.11"
+ checksum: 10c0/e3229938457cec617460c54ef4e90c8c254facc884729325d20ea35e3838bd273e4c611fc1f4a76f6d14c411e30d31b15e88eb4be87408615ff0aacc142511a2
+ languageName: node
+ linkType: hard
+
"express@npm:^4.14.0, express@npm:^4.17.1":
version: 4.18.2
resolution: "express@npm:4.18.2"
@@ -9653,6 +12102,42 @@ __metadata:
languageName: node
linkType: hard
+"express@npm:^5.2.1":
+ version: 5.2.1
+ resolution: "express@npm:5.2.1"
+ dependencies:
+ accepts: "npm:^2.0.0"
+ body-parser: "npm:^2.2.1"
+ content-disposition: "npm:^1.0.0"
+ content-type: "npm:^1.0.5"
+ cookie: "npm:^0.7.1"
+ cookie-signature: "npm:^1.2.1"
+ debug: "npm:^4.4.0"
+ depd: "npm:^2.0.0"
+ encodeurl: "npm:^2.0.0"
+ escape-html: "npm:^1.0.3"
+ etag: "npm:^1.8.1"
+ finalhandler: "npm:^2.1.0"
+ fresh: "npm:^2.0.0"
+ http-errors: "npm:^2.0.0"
+ merge-descriptors: "npm:^2.0.0"
+ mime-types: "npm:^3.0.0"
+ on-finished: "npm:^2.4.1"
+ once: "npm:^1.4.0"
+ parseurl: "npm:^1.3.3"
+ proxy-addr: "npm:^2.0.7"
+ qs: "npm:^6.14.0"
+ range-parser: "npm:^1.2.1"
+ router: "npm:^2.2.0"
+ send: "npm:^1.1.0"
+ serve-static: "npm:^2.2.0"
+ statuses: "npm:^2.0.1"
+ type-is: "npm:^2.0.1"
+ vary: "npm:^1.1.2"
+ checksum: 10c0/45e8c841ad188a41402ddcd1294901e861ee0819f632fb494f2ed344ef9c43315d294d443fb48d594e6586a3b779785120f43321417adaef8567316a55072949
+ languageName: node
+ linkType: hard
+
"ext@npm:^1.1.2":
version: 1.7.0
resolution: "ext@npm:1.7.0"
@@ -9776,6 +12261,18 @@ __metadata:
languageName: node
linkType: hard
+"fdir@npm:^6.2.0":
+ version: 6.5.0
+ resolution: "fdir@npm:6.5.0"
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+ checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f
+ languageName: node
+ linkType: hard
+
"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4":
version: 3.2.0
resolution: "fetch-blob@npm:3.2.0"
@@ -9795,6 +12292,15 @@ __metadata:
languageName: node
linkType: hard
+"figures@npm:^6.1.0":
+ version: 6.1.0
+ resolution: "figures@npm:6.1.0"
+ dependencies:
+ is-unicode-supported: "npm:^2.0.0"
+ checksum: 10c0/9159df4264d62ef447a3931537de92f5012210cf5135c35c010df50a2169377581378149abfe1eb238bd6acbba1c0d547b1f18e0af6eee49e30363cedaffcfe4
+ languageName: node
+ linkType: hard
+
"file-entry-cache@npm:^8.0.0":
version: 8.0.0
resolution: "file-entry-cache@npm:8.0.0"
@@ -9844,6 +12350,20 @@ __metadata:
languageName: node
linkType: hard
+"finalhandler@npm:^2.1.0":
+ version: 2.1.1
+ resolution: "finalhandler@npm:2.1.1"
+ dependencies:
+ debug: "npm:^4.4.0"
+ encodeurl: "npm:^2.0.0"
+ escape-html: "npm:^1.0.3"
+ on-finished: "npm:^2.4.1"
+ parseurl: "npm:^1.3.3"
+ statuses: "npm:^2.0.1"
+ checksum: 10c0/6bd664e21b7b2e79efcaace7d1a427169f61cce048fae68eb56290e6934e676b78e55d89f5998c5508871345bc59a61f47002dc505dc7288be68cceac1b701e2
+ languageName: node
+ linkType: hard
+
"find-up@npm:5.0.0, find-up@npm:^5.0.0":
version: 5.0.0
resolution: "find-up@npm:5.0.0"
@@ -10035,6 +12555,13 @@ __metadata:
languageName: node
linkType: hard
+"fresh@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "fresh@npm:2.0.0"
+ checksum: 10c0/0557548194cb9a809a435bf92bcfbc20c89e8b5eb38861b73ced36750437251e39a111fc3a18b98531be9dd91fe1411e4969f229dc579ec0251ce6c5d4900bbc
+ languageName: node
+ linkType: hard
+
"fs-constants@npm:^1.0.0":
version: 1.0.0
resolution: "fs-constants@npm:1.0.0"
@@ -10055,6 +12582,17 @@ __metadata:
languageName: node
linkType: hard
+"fs-extra@npm:^11.3.1":
+ version: 11.3.4
+ resolution: "fs-extra@npm:11.3.4"
+ dependencies:
+ graceful-fs: "npm:^4.2.0"
+ jsonfile: "npm:^6.0.1"
+ universalify: "npm:^2.0.0"
+ checksum: 10c0/e08276f767a62496ae97d711aaa692c6a478177f24a85979b6a2881c9db9c68b8c2ad5da0bcf92c0b2a474cea6e935ec245656441527958fd8372cb647087df0
+ languageName: node
+ linkType: hard
+
"fs-extra@npm:^4.0.2":
version: 4.0.3
resolution: "fs-extra@npm:4.0.3"
@@ -10190,6 +12728,13 @@ __metadata:
languageName: node
linkType: hard
+"fuzzysort@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "fuzzysort@npm:3.1.0"
+ checksum: 10c0/da9bb32de16f2a5c2c000b99031d9f4f8a01380c12d5d3b67296443a1152c55987ce3c4ddbfe97481b0e9b6f2fb77d61dceba29a93ad36ee23ef5bab6a31afb8
+ languageName: node
+ linkType: hard
+
"ganache@npm:7.9.1":
version: 7.9.1
resolution: "ganache@npm:7.9.1"
@@ -10220,6 +12765,13 @@ __metadata:
languageName: node
linkType: hard
+"generator-function@npm:^2.0.0":
+ version: 2.0.1
+ resolution: "generator-function@npm:2.0.1"
+ checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8
+ languageName: node
+ linkType: hard
+
"gensync@npm:^1.0.0-beta.2":
version: 1.0.0-beta.2
resolution: "gensync@npm:1.0.0-beta.2"
@@ -10241,6 +12793,13 @@ __metadata:
languageName: node
linkType: hard
+"get-east-asian-width@npm:^1.0.0":
+ version: 1.5.0
+ resolution: "get-east-asian-width@npm:1.5.0"
+ checksum: 10c0/bff8bbc8d81790b9477f7aa55b1806b9f082a8dc1359fff7bd8b96939622c86b729685afc2bfeb22def1fc6ef1e5228e4d87dd4e6da60bc43a5edfb03c4ee167
+ languageName: node
+ linkType: hard
+
"get-func-name@npm:^2.0.0":
version: 2.0.0
resolution: "get-func-name@npm:2.0.0"
@@ -10285,6 +12844,27 @@ __metadata:
languageName: node
linkType: hard
+"get-intrinsic@npm:^1.2.5":
+ version: 1.3.1
+ resolution: "get-intrinsic@npm:1.3.1"
+ dependencies:
+ async-function: "npm:^1.0.0"
+ async-generator-function: "npm:^1.0.0"
+ call-bind-apply-helpers: "npm:^1.0.2"
+ es-define-property: "npm:^1.0.1"
+ es-errors: "npm:^1.3.0"
+ es-object-atoms: "npm:^1.1.1"
+ function-bind: "npm:^1.1.2"
+ generator-function: "npm:^2.0.0"
+ get-proto: "npm:^1.0.1"
+ gopd: "npm:^1.2.0"
+ has-symbols: "npm:^1.1.0"
+ hasown: "npm:^2.0.2"
+ math-intrinsics: "npm:^1.1.0"
+ checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d
+ languageName: node
+ linkType: hard
+
"get-iterator@npm:^1.0.2":
version: 1.0.2
resolution: "get-iterator@npm:1.0.2"
@@ -10299,6 +12879,20 @@ __metadata:
languageName: node
linkType: hard
+"get-nonce@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "get-nonce@npm:1.0.1"
+ checksum: 10c0/2d7df55279060bf0568549e1ffc9b84bc32a32b7541675ca092dce56317cdd1a59a98dcc4072c9f6a980779440139a3221d7486f52c488e69dc0fd27b1efb162
+ languageName: node
+ linkType: hard
+
+"get-own-enumerable-keys@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "get-own-enumerable-keys@npm:1.0.0"
+ checksum: 10c0/3e14fbcf7cbb27a09f4335b3fe28ec4ac73254cd5007c141ff8e248c854fb1f4b44271fcc707c9aec1de7ae889eb28ffbd5b8a82f6abd9adb91df926fb7cec44
+ languageName: node
+ linkType: hard
+
"get-proto@npm:^1.0.1":
version: 1.0.1
resolution: "get-proto@npm:1.0.1"
@@ -10318,13 +12912,23 @@ __metadata:
languageName: node
linkType: hard
-"get-stream@npm:^6.0.1":
+"get-stream@npm:^6.0.0, get-stream@npm:^6.0.1":
version: 6.0.1
resolution: "get-stream@npm:6.0.1"
checksum: 10c0/49825d57d3fd6964228e6200a58169464b8e8970489b3acdc24906c782fb7f01f9f56f8e6653c4a50713771d6658f7cfe051e5eb8c12e334138c9c918b296341
languageName: node
linkType: hard
+"get-stream@npm:^9.0.0":
+ version: 9.0.1
+ resolution: "get-stream@npm:9.0.1"
+ dependencies:
+ "@sec-ant/readable-stream": "npm:^0.4.1"
+ is-stream: "npm:^4.0.1"
+ checksum: 10c0/d70e73857f2eea1826ac570c3a912757dcfbe8a718a033fa0c23e12ac8e7d633195b01710e0559af574cbb5af101009b42df7b6f6b29ceec8dbdf7291931b948
+ languageName: node
+ linkType: hard
+
"get-symbol-description@npm:^1.0.0":
version: 1.0.0
resolution: "get-symbol-description@npm:1.0.0"
@@ -10631,6 +13235,13 @@ __metadata:
languageName: node
linkType: hard
+"graphql@npm:^16.12.0":
+ version: 16.13.1
+ resolution: "graphql@npm:16.13.1"
+ checksum: 10c0/0c7a9aea59504fbf3e0674f13ddb82935780f2a388e1db0ef41c3711c0ff8cb0a871c50d30d2d5288f32b946af3570d6f9ba8d13b03a330336f27121f9ac7a6b
+ languageName: node
+ linkType: hard
+
"graphql@npm:^16.6.0":
version: 16.7.1
resolution: "graphql@npm:16.7.1"
@@ -10834,6 +13445,13 @@ __metadata:
languageName: node
linkType: hard
+"headers-polyfill@npm:^4.0.2":
+ version: 4.0.3
+ resolution: "headers-polyfill@npm:4.0.3"
+ checksum: 10c0/53e85b2c6385f8d411945fb890c5369f1469ce8aa32a6e8d28196df38568148de640c81cf88cbc7c67767103dd9acba48f4f891982da63178fc6e34560022afe
+ languageName: node
+ linkType: hard
+
"helmet@npm:^4.4.1":
version: 4.6.0
resolution: "helmet@npm:4.6.0"
@@ -10870,6 +13488,13 @@ __metadata:
languageName: node
linkType: hard
+"hono@npm:^4.11.4":
+ version: 4.12.8
+ resolution: "hono@npm:4.12.8"
+ checksum: 10c0/10b8a5012e362824d97d0f11895da6e7ba3195320399684f07b3743801986647aeb8395f189a8fefbe8d4567f943c23dc0ba73834ce9a848640a5d37d50f29b6
+ languageName: node
+ linkType: hard
+
"hosted-git-info@npm:^2.1.4":
version: 2.8.9
resolution: "hosted-git-info@npm:2.8.9"
@@ -10897,6 +13522,19 @@ __metadata:
languageName: node
linkType: hard
+"http-errors@npm:^2.0.0, http-errors@npm:^2.0.1, http-errors@npm:~2.0.1":
+ version: 2.0.1
+ resolution: "http-errors@npm:2.0.1"
+ dependencies:
+ depd: "npm:~2.0.0"
+ inherits: "npm:~2.0.4"
+ setprototypeof: "npm:~1.2.0"
+ statuses: "npm:~2.0.2"
+ toidentifier: "npm:~1.0.1"
+ checksum: 10c0/fb38906cef4f5c83952d97661fe14dc156cb59fe54812a42cd448fa57b5c5dfcb38a40a916957737bd6b87aab257c0648d63eb5b6a9ca9f548e105b6072712d4
+ languageName: node
+ linkType: hard
+
"http-https@npm:^1.0.0":
version: 1.0.0
resolution: "http-https@npm:1.0.0"
@@ -10984,6 +13622,30 @@ __metadata:
languageName: node
linkType: hard
+"https-proxy-agent@npm:^7.0.6":
+ version: 7.0.6
+ resolution: "https-proxy-agent@npm:7.0.6"
+ dependencies:
+ agent-base: "npm:^7.1.2"
+ debug: "npm:4"
+ checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac
+ languageName: node
+ linkType: hard
+
+"human-signals@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "human-signals@npm:2.1.0"
+ checksum: 10c0/695edb3edfcfe9c8b52a76926cd31b36978782062c0ed9b1192b36bebc75c4c87c82e178dfcb0ed0fc27ca59d434198aac0bd0be18f5781ded775604db22304a
+ languageName: node
+ linkType: hard
+
+"human-signals@npm:^8.0.1":
+ version: 8.0.1
+ resolution: "human-signals@npm:8.0.1"
+ checksum: 10c0/195ac607108c56253757717242e17cd2e21b29f06c5d2dad362e86c672bf2d096e8a3bbb2601841c376c2301c4ae7cff129e87f740aa4ebff1390c163114c7c4
+ languageName: node
+ linkType: hard
+
"iconv-lite@npm:0.4.24":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
@@ -11002,6 +13664,15 @@ __metadata:
languageName: node
linkType: hard
+"iconv-lite@npm:^0.7.0, iconv-lite@npm:~0.7.0":
+ version: 0.7.2
+ resolution: "iconv-lite@npm:0.7.2"
+ dependencies:
+ safer-buffer: "npm:>= 2.1.2 < 3.0.0"
+ checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722
+ languageName: node
+ linkType: hard
+
"idna-uts46-hx@npm:^2.3.1":
version: 2.3.1
resolution: "idna-uts46-hx@npm:2.3.1"
@@ -11039,7 +13710,7 @@ __metadata:
languageName: node
linkType: hard
-"ignore@npm:^5.3.1":
+"ignore@npm:^5.3.0, ignore@npm:^5.3.1":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337
@@ -11123,7 +13794,7 @@ __metadata:
languageName: node
linkType: hard
-"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3":
+"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3, inherits@npm:~2.0.4":
version: 2.0.4
resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
@@ -11248,6 +13919,13 @@ __metadata:
languageName: node
linkType: hard
+"ip-address@npm:10.1.0":
+ version: 10.1.0
+ resolution: "ip-address@npm:10.1.0"
+ checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566
+ languageName: node
+ linkType: hard
+
"ip-address@npm:^9.0.5":
version: 9.0.5
resolution: "ip-address@npm:9.0.5"
@@ -11485,6 +14163,15 @@ __metadata:
languageName: node
linkType: hard
+"is-docker@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "is-docker@npm:3.0.0"
+ bin:
+ is-docker: cli.js
+ checksum: 10c0/d2c4f8e6d3e34df75a5defd44991b6068afad4835bb783b902fa12d13ebdb8f41b2a199dcb0b5ed2cb78bfee9e4c0bbdb69c2d9646f4106464674d3e697a5856
+ languageName: node
+ linkType: hard
+
"is-electron@npm:^2.2.0":
version: 2.2.2
resolution: "is-electron@npm:2.2.2"
@@ -11547,6 +14234,31 @@ __metadata:
languageName: node
linkType: hard
+"is-in-ssh@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "is-in-ssh@npm:1.0.0"
+ checksum: 10c0/fbb4c25d85c543df09997fbe7aeca410ae0c839c5825bba2d4c672df765e9ce0e7558e781b371c0a21d6ef9bbac39b31875617a68eaaea5504438d07db9a2ffa
+ languageName: node
+ linkType: hard
+
+"is-inside-container@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "is-inside-container@npm:1.0.0"
+ dependencies:
+ is-docker: "npm:^3.0.0"
+ bin:
+ is-inside-container: cli.js
+ checksum: 10c0/a8efb0e84f6197e6ff5c64c52890fa9acb49b7b74fed4da7c95383965da6f0fa592b4dbd5e38a79f87fc108196937acdbcd758fcefc9b140e479b39ce1fcd1cd
+ languageName: node
+ linkType: hard
+
+"is-interactive@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "is-interactive@npm:2.0.0"
+ checksum: 10c0/801c8f6064f85199dc6bf99b5dd98db3282e930c3bc197b32f2c5b89313bb578a07d1b8a01365c4348c2927229234f3681eb861b9c2c92bee72ff397390fa600
+ languageName: node
+ linkType: hard
+
"is-ip@npm:3.0.0":
version: 3.0.0
resolution: "is-ip@npm:3.0.0"
@@ -11601,6 +14313,13 @@ __metadata:
languageName: node
linkType: hard
+"is-node-process@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "is-node-process@npm:1.2.0"
+ checksum: 10c0/5b24fda6776d00e42431d7bcd86bce81cb0b6cabeb944142fe7b077a54ada2e155066ad06dbe790abdb397884bdc3151e04a9707b8cd185099efbc79780573ed
+ languageName: node
+ linkType: hard
+
"is-number-object@npm:^1.0.4":
version: 1.0.7
resolution: "is-number-object@npm:1.0.7"
@@ -11624,6 +14343,13 @@ __metadata:
languageName: node
linkType: hard
+"is-obj@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "is-obj@npm:3.0.0"
+ checksum: 10c0/48d678fa15c56fd38353634ae2106a538827af9050211b18df13540dba0b38aa25c5cb498648a01311bf493a99ac3ce416576649b8cace10bcce7344611fa56a
+ languageName: node
+ linkType: hard
+
"is-plain-obj@npm:^2.1.0":
version: 2.1.0
resolution: "is-plain-obj@npm:2.1.0"
@@ -11631,7 +14357,7 @@ __metadata:
languageName: node
linkType: hard
-"is-plain-obj@npm:^4.0.0":
+"is-plain-obj@npm:^4.0.0, is-plain-obj@npm:^4.1.0":
version: 4.1.0
resolution: "is-plain-obj@npm:4.1.0"
checksum: 10c0/32130d651d71d9564dc88ba7e6fda0e91a1010a3694648e9f4f47bb6080438140696d3e3e15c741411d712e47ac9edc1a8a9de1fe76f3487b0d90be06ac9975e
@@ -11645,6 +14371,13 @@ __metadata:
languageName: node
linkType: hard
+"is-promise@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "is-promise@npm:4.0.0"
+ checksum: 10c0/ebd5c672d73db781ab33ccb155fb9969d6028e37414d609b115cc534654c91ccd061821d5b987eefaa97cf4c62f0b909bb2f04db88306de26e91bfe8ddc01503
+ languageName: node
+ linkType: hard
+
"is-regex@npm:^1.1.4":
version: 1.1.4
resolution: "is-regex@npm:1.1.4"
@@ -11655,6 +14388,13 @@ __metadata:
languageName: node
linkType: hard
+"is-regexp@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "is-regexp@npm:3.1.0"
+ checksum: 10c0/99dbaea41bddee2205db468c0946f5fee25cc4ae486333cb4d2b8095ab4b0a500e74ba61afd9e6e4f63ececcd55b4df5ae2a555b1c3e26308e516ff53c9533cd
+ languageName: node
+ linkType: hard
+
"is-shared-array-buffer@npm:^1.0.2":
version: 1.0.2
resolution: "is-shared-array-buffer@npm:1.0.2"
@@ -11664,6 +14404,20 @@ __metadata:
languageName: node
linkType: hard
+"is-stream@npm:^2.0.0":
+ version: 2.0.1
+ resolution: "is-stream@npm:2.0.1"
+ checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5
+ languageName: node
+ linkType: hard
+
+"is-stream@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "is-stream@npm:4.0.1"
+ checksum: 10c0/2706c7f19b851327ba374687bc4a3940805e14ca496dc672b9629e744d143b1ad9c6f1b162dece81c7bfbc0f83b32b61ccc19ad2e05aad2dd7af347408f60c7f
+ languageName: node
+ linkType: hard
+
"is-string@npm:^1.0.5, is-string@npm:^1.0.7":
version: 1.0.7
resolution: "is-string@npm:1.0.7"
@@ -11714,6 +14468,20 @@ __metadata:
languageName: node
linkType: hard
+"is-unicode-supported@npm:^1.3.0":
+ version: 1.3.0
+ resolution: "is-unicode-supported@npm:1.3.0"
+ checksum: 10c0/b8674ea95d869f6faabddc6a484767207058b91aea0250803cbf1221345cb0c56f466d4ecea375dc77f6633d248d33c47bd296fb8f4cdba0b4edba8917e83d8a
+ languageName: node
+ linkType: hard
+
+"is-unicode-supported@npm:^2.0.0":
+ version: 2.1.0
+ resolution: "is-unicode-supported@npm:2.1.0"
+ checksum: 10c0/a0f53e9a7c1fdbcf2d2ef6e40d4736fdffff1c9f8944c75e15425118ff3610172c87bf7bc6c34d3903b04be59790bb2212ddbe21ee65b5a97030fc50370545a5
+ languageName: node
+ linkType: hard
+
"is-upper-case@npm:^1.1.0":
version: 1.1.2
resolution: "is-upper-case@npm:1.1.2"
@@ -11753,6 +14521,15 @@ __metadata:
languageName: node
linkType: hard
+"is-wsl@npm:^3.1.0":
+ version: 3.1.1
+ resolution: "is-wsl@npm:3.1.1"
+ dependencies:
+ is-inside-container: "npm:^1.0.0"
+ checksum: 10c0/7e5023522bfb8f27de4de960b0d82c4a8146c0bddb186529a3616d78b5bbbfc19ef0c5fc60d0b3a3cc0bf95a415fbdedc18454310ea3049587c879b07ace5107
+ languageName: node
+ linkType: hard
+
"isarray@npm:0.0.1":
version: 0.0.1
resolution: "isarray@npm:0.0.1"
@@ -12103,10 +14880,26 @@ __metadata:
languageName: node
linkType: hard
-"jest-get-type@npm:^24.9.0":
- version: 24.9.0
- resolution: "jest-get-type@npm:24.9.0"
- checksum: 10c0/af1da287a14e5de5888b0114e92cd4042050852d32982d412e1465a8d69cb0a22702c7c491c56eb664e05a1391c1d6eeeb840e249a76d4f6159c402a4dfde56d
+"jest-get-type@npm:^24.9.0":
+ version: 24.9.0
+ resolution: "jest-get-type@npm:24.9.0"
+ checksum: 10c0/af1da287a14e5de5888b0114e92cd4042050852d32982d412e1465a8d69cb0a22702c7c491c56eb664e05a1391c1d6eeeb840e249a76d4f6159c402a4dfde56d
+ languageName: node
+ linkType: hard
+
+"jiti@npm:^2.6.1":
+ version: 2.6.1
+ resolution: "jiti@npm:2.6.1"
+ bin:
+ jiti: lib/jiti-cli.mjs
+ checksum: 10c0/79b2e96a8e623f66c1b703b98ec1b8be4500e1d217e09b09e343471bbb9c105381b83edbb979d01cef18318cc45ce6e153571b6c83122170eefa531c64b6789b
+ languageName: node
+ linkType: hard
+
+"jose@npm:^6.1.3":
+ version: 6.2.1
+ resolution: "jose@npm:6.2.1"
+ checksum: 10c0/456822e00a2ee6b1470fabadd694b237af64b9977aa8a47844aae2841298daefd79bb04ff328cbc3aa3c886761aa72b33bc5c1ad797b31b8368e77a89d09b779
languageName: node
linkType: hard
@@ -12230,6 +15023,13 @@ __metadata:
languageName: node
linkType: hard
+"json-schema-typed@npm:^8.0.2":
+ version: 8.0.2
+ resolution: "json-schema-typed@npm:8.0.2"
+ checksum: 10c0/89f5e2fb1495483b705c027203c07277ee6bf2665165ad25a9cb55de5af7f72570326d13d32565180781e4083ad5c9688102f222baed7b353c2f39c1e02b0428
+ languageName: node
+ linkType: hard
+
"json-schema@npm:0.4.0":
version: 0.4.0
resolution: "json-schema@npm:0.4.0"
@@ -12408,7 +15208,14 @@ __metadata:
languageName: node
linkType: hard
-"kleur@npm:^4.0.3":
+"kleur@npm:^3.0.3":
+ version: 3.0.3
+ resolution: "kleur@npm:3.0.3"
+ checksum: 10c0/cd3a0b8878e7d6d3799e54340efe3591ca787d9f95f109f28129bdd2915e37807bf8918bb295ab86afb8c82196beec5a1adcaf29042ce3f2bd932b038fe3aa4b
+ languageName: node
+ linkType: hard
+
+"kleur@npm:^4.0.3, kleur@npm:^4.1.5":
version: 4.1.5
resolution: "kleur@npm:4.1.5"
checksum: 10c0/e9de6cb49657b6fa70ba2d1448fd3d691a5c4370d8f7bbf1c2f64c24d461270f2117e1b0afe8cb3114f13bbd8e51de158c2a224953960331904e636a5e4c0f2a
@@ -12659,6 +15466,126 @@ __metadata:
languageName: node
linkType: hard
+"lightningcss-android-arm64@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-android-arm64@npm:1.31.1"
+ conditions: os=android & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"lightningcss-darwin-arm64@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-darwin-arm64@npm:1.31.1"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"lightningcss-darwin-x64@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-darwin-x64@npm:1.31.1"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
+"lightningcss-freebsd-x64@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-freebsd-x64@npm:1.31.1"
+ conditions: os=freebsd & cpu=x64
+ languageName: node
+ linkType: hard
+
+"lightningcss-linux-arm-gnueabihf@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-linux-arm-gnueabihf@npm:1.31.1"
+ conditions: os=linux & cpu=arm
+ languageName: node
+ linkType: hard
+
+"lightningcss-linux-arm64-gnu@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-linux-arm64-gnu@npm:1.31.1"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"lightningcss-linux-arm64-musl@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-linux-arm64-musl@npm:1.31.1"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"lightningcss-linux-x64-gnu@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-linux-x64-gnu@npm:1.31.1"
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"lightningcss-linux-x64-musl@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-linux-x64-musl@npm:1.31.1"
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"lightningcss-win32-arm64-msvc@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-win32-arm64-msvc@npm:1.31.1"
+ conditions: os=win32 & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"lightningcss-win32-x64-msvc@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss-win32-x64-msvc@npm:1.31.1"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
+"lightningcss@npm:1.31.1":
+ version: 1.31.1
+ resolution: "lightningcss@npm:1.31.1"
+ dependencies:
+ detect-libc: "npm:^2.0.3"
+ lightningcss-android-arm64: "npm:1.31.1"
+ lightningcss-darwin-arm64: "npm:1.31.1"
+ lightningcss-darwin-x64: "npm:1.31.1"
+ lightningcss-freebsd-x64: "npm:1.31.1"
+ lightningcss-linux-arm-gnueabihf: "npm:1.31.1"
+ lightningcss-linux-arm64-gnu: "npm:1.31.1"
+ lightningcss-linux-arm64-musl: "npm:1.31.1"
+ lightningcss-linux-x64-gnu: "npm:1.31.1"
+ lightningcss-linux-x64-musl: "npm:1.31.1"
+ lightningcss-win32-arm64-msvc: "npm:1.31.1"
+ lightningcss-win32-x64-msvc: "npm:1.31.1"
+ dependenciesMeta:
+ lightningcss-android-arm64:
+ optional: true
+ lightningcss-darwin-arm64:
+ optional: true
+ lightningcss-darwin-x64:
+ optional: true
+ lightningcss-freebsd-x64:
+ optional: true
+ lightningcss-linux-arm-gnueabihf:
+ optional: true
+ lightningcss-linux-arm64-gnu:
+ optional: true
+ lightningcss-linux-arm64-musl:
+ optional: true
+ lightningcss-linux-x64-gnu:
+ optional: true
+ lightningcss-linux-x64-musl:
+ optional: true
+ lightningcss-win32-arm64-msvc:
+ optional: true
+ lightningcss-win32-x64-msvc:
+ optional: true
+ checksum: 10c0/c6754b305d4a73652e472fc0d7d65384a6e16c336ea61068eca60de2a45bd5c30abbf012358b82eac56ee98b5d88028932cda5268ff61967cffa400b9e7ee2ba
+ languageName: node
+ linkType: hard
+
"lines-and-columns@npm:^1.1.6":
version: 1.2.4
resolution: "lines-and-columns@npm:1.2.4"
@@ -12793,6 +15720,16 @@ __metadata:
languageName: node
linkType: hard
+"log-symbols@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "log-symbols@npm:6.0.0"
+ dependencies:
+ chalk: "npm:^5.3.0"
+ is-unicode-supported: "npm:^1.3.0"
+ checksum: 10c0/36636cacedba8f067d2deb4aad44e91a89d9efb3ead27e1846e7b82c9a10ea2e3a7bd6ce28a7ca616bebc60954ff25c67b0f92d20a6a746bb3cc52c3701891f6
+ languageName: node
+ linkType: hard
+
"loglevel@npm:^1.6.8":
version: 1.8.1
resolution: "loglevel@npm:1.8.1"
@@ -12944,6 +15881,24 @@ __metadata:
languageName: node
linkType: hard
+"lucide-react@npm:^0.577.0":
+ version: 0.577.0
+ resolution: "lucide-react@npm:0.577.0"
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ checksum: 10c0/f5f56d66d5072b69ab74af2196833e35b888c18d92906d75aff81afc0aeabf00a4092a5b9dbc1bddd250bf54ec3c5d5933c7353589b5cb7efd75b21651cda399
+ languageName: node
+ linkType: hard
+
+"magic-string@npm:^0.30.21":
+ version: 0.30.21
+ resolution: "magic-string@npm:0.30.21"
+ dependencies:
+ "@jridgewell/sourcemap-codec": "npm:^1.5.5"
+ checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a
+ languageName: node
+ linkType: hard
+
"magic-string@npm:^0.30.5":
version: 0.30.5
resolution: "magic-string@npm:0.30.5"
@@ -13061,6 +16016,13 @@ __metadata:
languageName: node
linkType: hard
+"media-typer@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "media-typer@npm:1.1.0"
+ checksum: 10c0/7b4baa40b25964bb90e2121ee489ec38642127e48d0cc2b6baa442688d3fde6262bfdca86d6bbf6ba708784afcac168c06840c71facac70e390f5f759ac121b9
+ languageName: node
+ linkType: hard
+
"memdown@npm:1.4.1":
version: 1.4.1
resolution: "memdown@npm:1.4.1"
@@ -13137,6 +16099,13 @@ __metadata:
languageName: node
linkType: hard
+"merge-descriptors@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "merge-descriptors@npm:2.0.0"
+ checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3
+ languageName: node
+ linkType: hard
+
"merge-options@npm:^3.0.4":
version: 3.0.4
resolution: "merge-options@npm:3.0.4"
@@ -13146,6 +16115,13 @@ __metadata:
languageName: node
linkType: hard
+"merge-stream@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "merge-stream@npm:2.0.0"
+ checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5
+ languageName: node
+ linkType: hard
+
"merge2@npm:^1.3.0":
version: 1.4.1
resolution: "merge2@npm:1.4.1"
@@ -13442,6 +16418,13 @@ __metadata:
languageName: node
linkType: hard
+"mime-db@npm:^1.54.0":
+ version: 1.54.0
+ resolution: "mime-db@npm:1.54.0"
+ checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284
+ languageName: node
+ linkType: hard
+
"mime-types@npm:^2.1.12, mime-types@npm:^2.1.16, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
@@ -13451,6 +16434,15 @@ __metadata:
languageName: node
linkType: hard
+"mime-types@npm:^3.0.0, mime-types@npm:^3.0.2":
+ version: 3.0.2
+ resolution: "mime-types@npm:3.0.2"
+ dependencies:
+ mime-db: "npm:^1.54.0"
+ checksum: 10c0/35a0dd1035d14d185664f346efcdb72e93ef7a9b6e9ae808bd1f6358227010267fab52657b37562c80fc888ff76becb2b2938deb5e730818b7983bf8bd359767
+ languageName: node
+ linkType: hard
+
"mime@npm:1.6.0, mime@npm:^1.6.0":
version: 1.6.0
resolution: "mime@npm:1.6.0"
@@ -13474,6 +16466,13 @@ __metadata:
languageName: node
linkType: hard
+"mimic-function@npm:^5.0.0":
+ version: 5.0.1
+ resolution: "mimic-function@npm:5.0.1"
+ checksum: 10c0/f3d9464dd1816ecf6bdf2aec6ba32c0728022039d992f178237d8e289b48764fee4131319e72eedd4f7f094e22ded0af836c3187a7edc4595d28dd74368fd81d
+ languageName: node
+ linkType: hard
+
"mimic-response@npm:^1.0.0":
version: 1.0.1
resolution: "mimic-response@npm:1.0.1"
@@ -13520,6 +16519,15 @@ __metadata:
languageName: node
linkType: hard
+"minimatch@npm:^10.0.1":
+ version: 10.2.4
+ resolution: "minimatch@npm:10.2.4"
+ dependencies:
+ brace-expansion: "npm:^5.0.2"
+ checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945
+ languageName: node
+ linkType: hard
+
"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
@@ -13895,6 +16903,39 @@ __metadata:
languageName: node
linkType: hard
+"msw@npm:^2.10.4":
+ version: 2.12.11
+ resolution: "msw@npm:2.12.11"
+ dependencies:
+ "@inquirer/confirm": "npm:^5.0.0"
+ "@mswjs/interceptors": "npm:^0.41.2"
+ "@open-draft/deferred-promise": "npm:^2.2.0"
+ "@types/statuses": "npm:^2.0.6"
+ cookie: "npm:^1.0.2"
+ graphql: "npm:^16.12.0"
+ headers-polyfill: "npm:^4.0.2"
+ is-node-process: "npm:^1.2.0"
+ outvariant: "npm:^1.4.3"
+ path-to-regexp: "npm:^6.3.0"
+ picocolors: "npm:^1.1.1"
+ rettime: "npm:^0.10.1"
+ statuses: "npm:^2.0.2"
+ strict-event-emitter: "npm:^0.5.1"
+ tough-cookie: "npm:^6.0.0"
+ type-fest: "npm:^5.2.0"
+ until-async: "npm:^3.0.2"
+ yargs: "npm:^17.7.2"
+ peerDependencies:
+ typescript: ">= 4.8.x"
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ bin:
+ msw: cli/index.js
+ checksum: 10c0/2ed4f18bacae0e83ac5726acd3efb2f96641840afd4dd4d1cf35cf3ad48b11e83f8d90f099da33715fd3113761933aa2a2df55b1447d28df1604d29cc72e6864
+ languageName: node
+ linkType: hard
+
"multibase@npm:^0.7.0":
version: 0.7.0
resolution: "multibase@npm:0.7.0"
@@ -14017,6 +17058,13 @@ __metadata:
languageName: node
linkType: hard
+"mute-stream@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "mute-stream@npm:2.0.0"
+ checksum: 10c0/2cf48a2087175c60c8dcdbc619908b49c07f7adcfc37d29236b0c5c612d6204f789104c98cc44d38acab7b3c96f4a3ec2cfdc4934d0738d876dbefa2a12c69f4
+ languageName: node
+ linkType: hard
+
"nan@npm:^2.17.0":
version: 2.17.0
resolution: "nan@npm:2.17.0"
@@ -14133,6 +17181,23 @@ __metadata:
languageName: node
linkType: hard
+"negotiator@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "negotiator@npm:1.0.0"
+ checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b
+ languageName: node
+ linkType: hard
+
+"next-themes@npm:^0.4.6":
+ version: 0.4.6
+ resolution: "next-themes@npm:0.4.6"
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ checksum: 10c0/83590c11d359ce7e4ced14f6ea9dd7a691d5ce6843fe2dc520fc27e29ae1c535118478d03e7f172609c41b1ef1b8da6b8dd2d2acd6cd79cac1abbdbd5b99f2c4
+ languageName: node
+ linkType: hard
+
"next-tick@npm:1, next-tick@npm:^1.1.0":
version: 1.1.0
resolution: "next-tick@npm:1.1.0"
@@ -14471,6 +17536,25 @@ __metadata:
languageName: node
linkType: hard
+"npm-run-path@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "npm-run-path@npm:4.0.1"
+ dependencies:
+ path-key: "npm:^3.0.0"
+ checksum: 10c0/6f9353a95288f8455cf64cbeb707b28826a7f29690244c1e4bb61ec573256e021b6ad6651b394eb1ccfd00d6ec50147253aba2c5fe58a57ceb111fad62c519ac
+ languageName: node
+ linkType: hard
+
+"npm-run-path@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "npm-run-path@npm:6.0.0"
+ dependencies:
+ path-key: "npm:^4.0.0"
+ unicorn-magic: "npm:^0.3.0"
+ checksum: 10c0/b223c8a0dcd608abf95363ea5c3c0ccc3cd877daf0102eaf1b0f2390d6858d8337fbb7c443af2403b067a7d2c116d10691ecd22ab3c5273c44da1ff8d07753bd
+ languageName: node
+ linkType: hard
+
"number-is-nan@npm:^1.0.0":
version: 1.0.1
resolution: "number-is-nan@npm:1.0.1"
@@ -14509,6 +17593,13 @@ __metadata:
languageName: node
linkType: hard
+"object-inspect@npm:^1.13.3":
+ version: 1.13.4
+ resolution: "object-inspect@npm:1.13.4"
+ checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692
+ languageName: node
+ linkType: hard
+
"object-keys@npm:^1.1.1":
version: 1.1.1
resolution: "object-keys@npm:1.1.1"
@@ -14516,6 +17607,13 @@ __metadata:
languageName: node
linkType: hard
+"object-treeify@npm:1.1.33":
+ version: 1.1.33
+ resolution: "object-treeify@npm:1.1.33"
+ checksum: 10c0/5b735ac552200bf14f9892ce58295303e8d15a8cc7a0fd4fe6ff99923ab0c196fb70a870ab2a0eefc6820c4acb49e614b88c72d344b9c6bd22584a3efbd386fe
+ languageName: node
+ linkType: hard
+
"object.assign@npm:^4.1.4":
version: 4.1.4
resolution: "object.assign@npm:4.1.4"
@@ -14537,7 +17635,7 @@ __metadata:
languageName: node
linkType: hard
-"on-finished@npm:2.4.1":
+"on-finished@npm:2.4.1, on-finished@npm:^2.4.1":
version: 2.4.1
resolution: "on-finished@npm:2.4.1"
dependencies:
@@ -14571,6 +17669,29 @@ __metadata:
languageName: node
linkType: hard
+"onetime@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "onetime@npm:7.0.0"
+ dependencies:
+ mimic-function: "npm:^5.0.0"
+ checksum: 10c0/5cb9179d74b63f52a196a2e7037ba2b9a893245a5532d3f44360012005c9cadb60851d56716ebff18a6f47129dab7168022445df47c2aff3b276d92585ed1221
+ languageName: node
+ linkType: hard
+
+"open@npm:^11.0.0":
+ version: 11.0.0
+ resolution: "open@npm:11.0.0"
+ dependencies:
+ default-browser: "npm:^5.4.0"
+ define-lazy-prop: "npm:^3.0.0"
+ is-in-ssh: "npm:^1.0.0"
+ is-inside-container: "npm:^1.0.0"
+ powershell-utils: "npm:^0.1.0"
+ wsl-utils: "npm:^0.3.0"
+ checksum: 10c0/7aeeda4131268ed90f90e7728dda5c46bb0c6205b27a4be3e86ea33593e30dd393423e20e31c00802a8e635ef59becaee33ef9749a8ceb027567cd253e9e7b1e
+ languageName: node
+ linkType: hard
+
"optionator@npm:^0.9.3":
version: 0.9.3
resolution: "optionator@npm:0.9.3"
@@ -14585,6 +17706,23 @@ __metadata:
languageName: node
linkType: hard
+"ora@npm:^8.2.0":
+ version: 8.2.0
+ resolution: "ora@npm:8.2.0"
+ dependencies:
+ chalk: "npm:^5.3.0"
+ cli-cursor: "npm:^5.0.0"
+ cli-spinners: "npm:^2.9.2"
+ is-interactive: "npm:^2.0.0"
+ is-unicode-supported: "npm:^2.0.0"
+ log-symbols: "npm:^6.0.0"
+ stdin-discarder: "npm:^0.2.2"
+ string-width: "npm:^7.2.0"
+ strip-ansi: "npm:^7.1.0"
+ checksum: 10c0/7d9291255db22e293ea164f520b6042a3e906576ab06c9cf408bf9ef5664ba0a9f3bd258baa4ada058cfcc2163ef9b6696d51237a866682ce33295349ba02c3a
+ languageName: node
+ linkType: hard
+
"original-require@npm:^1.0.1":
version: 1.0.1
resolution: "original-require@npm:1.0.1"
@@ -14608,6 +17746,13 @@ __metadata:
languageName: node
linkType: hard
+"outvariant@npm:^1.4.0, outvariant@npm:^1.4.3":
+ version: 1.4.3
+ resolution: "outvariant@npm:1.4.3"
+ checksum: 10c0/5976ca7740349cb8c71bd3382e2a762b1aeca6f33dc984d9d896acdf3c61f78c3afcf1bfe9cc633a7b3c4b295ec94d292048f83ea2b2594fae4496656eba992c
+ languageName: node
+ linkType: hard
+
"p-cancelable@npm:^2.0.0":
version: 2.1.1
resolution: "p-cancelable@npm:2.1.1"
@@ -14834,6 +17979,13 @@ __metadata:
languageName: node
linkType: hard
+"parse-ms@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "parse-ms@npm:4.0.0"
+ checksum: 10c0/a7900f4f1ebac24cbf5e9708c16fb2fd482517fad353aecd7aefb8c2ba2f85ce017913ccb8925d231770404780df46244ea6fec598b3bde6490882358b4d2d16
+ languageName: node
+ linkType: hard
+
"parse-passwd@npm:^1.0.0":
version: 1.0.0
resolution: "parse-passwd@npm:1.0.0"
@@ -14865,6 +18017,13 @@ __metadata:
languageName: node
linkType: hard
+"path-browserify@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "path-browserify@npm:1.0.1"
+ checksum: 10c0/8b8c3fd5c66bd340272180590ae4ff139769e9ab79522e2eb82e3d571a89b8117c04147f65ad066dccfb42fcad902e5b7d794b3d35e0fd840491a8ddbedf8c66
+ languageName: node
+ linkType: hard
+
"path-case@npm:^2.1.0":
version: 2.1.1
resolution: "path-case@npm:2.1.1"
@@ -14911,13 +18070,20 @@ __metadata:
languageName: node
linkType: hard
-"path-key@npm:^3.1.0":
+"path-key@npm:^3.0.0, path-key@npm:^3.1.0":
version: 3.1.1
resolution: "path-key@npm:3.1.1"
checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c
languageName: node
linkType: hard
+"path-key@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "path-key@npm:4.0.0"
+ checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3
+ languageName: node
+ linkType: hard
+
"path-parse@npm:^1.0.5, path-parse@npm:^1.0.7":
version: 1.0.7
resolution: "path-parse@npm:1.0.7"
@@ -14961,6 +18127,20 @@ __metadata:
languageName: node
linkType: hard
+"path-to-regexp@npm:^6.3.0":
+ version: 6.3.0
+ resolution: "path-to-regexp@npm:6.3.0"
+ checksum: 10c0/73b67f4638b41cde56254e6354e46ae3a2ebc08279583f6af3d96fe4664fc75788f74ed0d18ca44fa4a98491b69434f9eee73b97bb5314bd1b5adb700f5c18d6
+ languageName: node
+ linkType: hard
+
+"path-to-regexp@npm:^8.0.0":
+ version: 8.3.0
+ resolution: "path-to-regexp@npm:8.3.0"
+ checksum: 10c0/ee1544a73a3f294a97a4c663b0ce71bbf1621d732d80c9c9ed201b3e911a86cb628ebad691b9d40f40a3742fe22011e5a059d8eed2cf63ec2cb94f6fb4efe67c
+ languageName: node
+ linkType: hard
+
"path-type@npm:^1.0.0":
version: 1.1.0
resolution: "path-type@npm:1.1.0"
@@ -15065,6 +18245,13 @@ __metadata:
languageName: node
linkType: hard
+"pkce-challenge@npm:^5.0.0":
+ version: 5.0.1
+ resolution: "pkce-challenge@npm:5.0.1"
+ checksum: 10c0/207f4cb976682f27e8324eb49cf71937c98fbb8341a0b8f6142bc6f664825b30e049a54a21b5c034e823ee3c3d412f10d74bd21de78e17452a6a496c2991f57c
+ languageName: node
+ linkType: hard
+
"pkg-up@npm:^3.1.0":
version: 3.1.0
resolution: "pkg-up@npm:3.1.0"
@@ -15104,6 +18291,16 @@ __metadata:
languageName: node
linkType: hard
+"postcss-selector-parser@npm:^7.1.0":
+ version: 7.1.1
+ resolution: "postcss-selector-parser@npm:7.1.1"
+ dependencies:
+ cssesc: "npm:^3.0.0"
+ util-deprecate: "npm:^1.0.2"
+ checksum: 10c0/02d3b1589ddcddceed4b583b098b95a7266dacd5135f041e5d913ebb48e874fd333a36e564cc9a2ec426a464cb18db11cb192ac76247aced5eba8c951bf59507
+ languageName: node
+ linkType: hard
+
"postcss-value-parser@npm:^3.3.0":
version: 3.3.1
resolution: "postcss-value-parser@npm:3.3.1"
@@ -15133,6 +18330,17 @@ __metadata:
languageName: node
linkType: hard
+"postcss@npm:^8.5.6":
+ version: 8.5.8
+ resolution: "postcss@npm:8.5.8"
+ dependencies:
+ nanoid: "npm:^3.3.11"
+ picocolors: "npm:^1.1.1"
+ source-map-js: "npm:^1.2.1"
+ checksum: 10c0/dd918f7127ee7c60a0295bae2e72b3787892296e1d1c3c564d7a2a00c68d8df83cadc3178491259daa19ccc54804fb71ed8c937c6787e08d8bd4bedf8d17044c
+ languageName: node
+ linkType: hard
+
"pouchdb-abstract-mapreduce@npm:7.3.1":
version: 7.3.1
resolution: "pouchdb-abstract-mapreduce@npm:7.3.1"
@@ -15355,6 +18563,13 @@ __metadata:
languageName: node
linkType: hard
+"powershell-utils@npm:^0.1.0":
+ version: 0.1.0
+ resolution: "powershell-utils@npm:0.1.0"
+ checksum: 10c0/a64713cf3583259c9ed6be211c06b4b19e8608bcb0f7f6287ffac0a95b8c7582b6b662eea0e201fd659492c8e9f9c5fd0bfc4579645c5add9c1a600075621c95
+ languageName: node
+ linkType: hard
+
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@@ -15408,6 +18623,15 @@ __metadata:
languageName: node
linkType: hard
+"pretty-ms@npm:^9.2.0":
+ version: 9.3.0
+ resolution: "pretty-ms@npm:9.3.0"
+ dependencies:
+ parse-ms: "npm:^4.0.0"
+ checksum: 10c0/555ea39a1de48a30601938aedb76d682871d33b6dee015281c37108921514b11e1792928b1648c2e5589acc73c8ef0fb5e585fb4c718e340a28b86799e90fb34
+ languageName: node
+ linkType: hard
+
"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0":
version: 4.2.0
resolution: "proc-log@npm:4.2.0"
@@ -15462,6 +18686,16 @@ __metadata:
languageName: node
linkType: hard
+"prompts@npm:^2.4.2":
+ version: 2.4.2
+ resolution: "prompts@npm:2.4.2"
+ dependencies:
+ kleur: "npm:^3.0.3"
+ sisteransi: "npm:^1.0.5"
+ checksum: 10c0/16f1ac2977b19fe2cf53f8411cc98db7a3c8b115c479b2ca5c82b5527cd937aa405fa04f9a5960abeb9daef53191b53b4d13e35c1f5d50e8718c76917c5f1ea4
+ languageName: node
+ linkType: hard
+
"prop-types-extra@npm:^1.1.0":
version: 1.1.1
resolution: "prop-types-extra@npm:1.1.1"
@@ -15535,7 +18769,7 @@ __metadata:
languageName: node
linkType: hard
-"proxy-addr@npm:~2.0.7":
+"proxy-addr@npm:^2.0.7, proxy-addr@npm:~2.0.7":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"
dependencies:
@@ -15681,6 +18915,15 @@ __metadata:
languageName: node
linkType: hard
+"qs@npm:^6.14.0, qs@npm:^6.14.1":
+ version: 6.15.0
+ resolution: "qs@npm:6.15.0"
+ dependencies:
+ side-channel: "npm:^1.1.0"
+ checksum: 10c0/ff341078a78a991d8a48b4524d52949211447b4b1ad907f489cac0770cbc346a28e47304455c0320e5fb000f8762d64b03331e3b71865f663bf351bcba8cdb4b
+ languageName: node
+ linkType: hard
+
"qs@npm:~6.5.2":
version: 6.5.3
resolution: "qs@npm:6.5.3"
@@ -15741,6 +18984,79 @@ __metadata:
languageName: node
linkType: hard
+"radix-ui@npm:^1.4.3":
+ version: 1.4.3
+ resolution: "radix-ui@npm:1.4.3"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-accessible-icon": "npm:1.1.7"
+ "@radix-ui/react-accordion": "npm:1.2.12"
+ "@radix-ui/react-alert-dialog": "npm:1.1.15"
+ "@radix-ui/react-arrow": "npm:1.1.7"
+ "@radix-ui/react-aspect-ratio": "npm:1.1.7"
+ "@radix-ui/react-avatar": "npm:1.1.10"
+ "@radix-ui/react-checkbox": "npm:1.3.3"
+ "@radix-ui/react-collapsible": "npm:1.1.12"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-context-menu": "npm:2.2.16"
+ "@radix-ui/react-dialog": "npm:1.1.15"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-dropdown-menu": "npm:2.1.16"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-form": "npm:0.1.8"
+ "@radix-ui/react-hover-card": "npm:1.1.15"
+ "@radix-ui/react-label": "npm:2.1.7"
+ "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-menubar": "npm:1.1.16"
+ "@radix-ui/react-navigation-menu": "npm:1.2.14"
+ "@radix-ui/react-one-time-password-field": "npm:0.1.8"
+ "@radix-ui/react-password-toggle-field": "npm:0.1.3"
+ "@radix-ui/react-popover": "npm:1.1.15"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-progress": "npm:1.1.7"
+ "@radix-ui/react-radio-group": "npm:1.3.8"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-scroll-area": "npm:1.2.10"
+ "@radix-ui/react-select": "npm:2.2.6"
+ "@radix-ui/react-separator": "npm:1.1.7"
+ "@radix-ui/react-slider": "npm:1.3.6"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-switch": "npm:1.2.6"
+ "@radix-ui/react-tabs": "npm:1.1.13"
+ "@radix-ui/react-toast": "npm:1.2.15"
+ "@radix-ui/react-toggle": "npm:1.1.10"
+ "@radix-ui/react-toggle-group": "npm:1.1.11"
+ "@radix-ui/react-toolbar": "npm:1.1.11"
+ "@radix-ui/react-tooltip": "npm:1.2.8"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-effect-event": "npm:0.0.2"
+ "@radix-ui/react-use-escape-keydown": "npm:1.1.1"
+ "@radix-ui/react-use-is-hydrated": "npm:0.1.0"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/6f227ece95a4804e85627bd20cb8c7f244ac38cead34f941210a9a9d1adc02c793850c6339b39db94ac0d57e30e95e6f7bbba2bfce951e98a91c88376a6e7da7
+ languageName: node
+ linkType: hard
+
"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0":
version: 2.1.0
resolution: "randombytes@npm:2.1.0"
@@ -15760,7 +19076,7 @@ __metadata:
languageName: node
linkType: hard
-"range-parser@npm:~1.2.1":
+"range-parser@npm:^1.2.1, range-parser@npm:~1.2.1":
version: 1.2.1
resolution: "range-parser@npm:1.2.1"
checksum: 10c0/96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0
@@ -15791,6 +19107,18 @@ __metadata:
languageName: node
linkType: hard
+"raw-body@npm:^3.0.0, raw-body@npm:^3.0.1":
+ version: 3.0.2
+ resolution: "raw-body@npm:3.0.2"
+ dependencies:
+ bytes: "npm:~3.1.2"
+ http-errors: "npm:~2.0.1"
+ iconv-lite: "npm:~0.7.0"
+ unpipe: "npm:~1.0.0"
+ checksum: 10c0/d266678d08e1e7abea62c0ce5864344e980fa81c64f6b481e9842c5beaed2cdcf975f658a3ccd67ad35fc919c1f6664ccc106067801850286a6cbe101de89f29
+ languageName: node
+ linkType: hard
+
"react-bootstrap@npm:^2.10":
version: 2.10.10
resolution: "react-bootstrap@npm:2.10.10"
@@ -15931,6 +19259,41 @@ __metadata:
languageName: node
linkType: hard
+"react-remove-scroll-bar@npm:^2.3.7":
+ version: 2.3.8
+ resolution: "react-remove-scroll-bar@npm:2.3.8"
+ dependencies:
+ react-style-singleton: "npm:^2.2.2"
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/9a0675c66cbb52c325bdbfaed80987a829c4504cefd8ff2dd3b6b3afc9a1500b8ec57b212e92c1fb654396d07bbe18830a8146fe77677d2a29ce40b5e1f78654
+ languageName: node
+ linkType: hard
+
+"react-remove-scroll@npm:^2.6.3":
+ version: 2.7.2
+ resolution: "react-remove-scroll@npm:2.7.2"
+ dependencies:
+ react-remove-scroll-bar: "npm:^2.3.7"
+ react-style-singleton: "npm:^2.2.3"
+ tslib: "npm:^2.1.0"
+ use-callback-ref: "npm:^1.3.3"
+ use-sidecar: "npm:^1.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/b5f3315bead75e72853f492c0b51ba8fb4fa09a4534d7fc42d6fcd59ca3e047cf213279ffc1e35b337e314ef5a04cb2b12544c85e0078802271731c27c09e5aa
+ languageName: node
+ linkType: hard
+
"react-router-dom@npm:^6.14.1":
version: 6.14.2
resolution: "react-router-dom@npm:6.14.2"
@@ -15955,6 +19318,22 @@ __metadata:
languageName: node
linkType: hard
+"react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3":
+ version: 2.2.3
+ resolution: "react-style-singleton@npm:2.2.3"
+ dependencies:
+ get-nonce: "npm:^1.0.0"
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/841938ff16d16a6b76895f4cb2e1fea957e5fe3b30febbf03a54892dae1c9153f2383e231dea0b3ba41192ad2f2849448fa859caccd288943bce32639e971bee
+ languageName: node
+ linkType: hard
+
"react-switch@npm:^5.0.1":
version: 5.0.1
resolution: "react-switch@npm:5.0.1"
@@ -16095,6 +19474,19 @@ __metadata:
languageName: node
linkType: hard
+"recast@npm:^0.23.11":
+ version: 0.23.11
+ resolution: "recast@npm:0.23.11"
+ dependencies:
+ ast-types: "npm:^0.16.1"
+ esprima: "npm:~4.0.0"
+ source-map: "npm:~0.6.1"
+ tiny-invariant: "npm:^1.3.3"
+ tslib: "npm:^2.0.1"
+ checksum: 10c0/45b520a8f0868a5a24ecde495be9de3c48e69a54295d82a7331106554b75cfba75d16c909959d056e9ceed47a1be5e061e2db8b9ecbcd6ba44c2f3ef9a47bd18
+ languageName: node
+ linkType: hard
+
"receptacle@npm:^1.3.2":
version: 1.3.2
resolution: "receptacle@npm:1.3.2"
@@ -16416,6 +19808,16 @@ __metadata:
languageName: node
linkType: hard
+"restore-cursor@npm:^5.0.0":
+ version: 5.1.0
+ resolution: "restore-cursor@npm:5.1.0"
+ dependencies:
+ onetime: "npm:^7.0.0"
+ signal-exit: "npm:^4.1.0"
+ checksum: 10c0/c2ba89131eea791d1b25205bdfdc86699767e2b88dee2a590b1a6caa51737deac8bad0260a5ded2f7c074b7db2f3a626bcf1fcf3cdf35974cbeea5e2e6764f60
+ languageName: node
+ linkType: hard
+
"retimer@npm:^3.0.0":
version: 3.0.0
resolution: "retimer@npm:3.0.0"
@@ -16437,6 +19839,13 @@ __metadata:
languageName: node
linkType: hard
+"rettime@npm:^0.10.1":
+ version: 0.10.1
+ resolution: "rettime@npm:0.10.1"
+ checksum: 10c0/94fb30cd13684386c70301c4cff4391bc0c6dc7aeac49364fdfeeaba167897bdb28a58bbb46d1a415f1c5c6240fda3f765cb329e471f37fdc513c739f0b04fbe
+ languageName: node
+ linkType: hard
+
"reusify@npm:^1.0.4":
version: 1.0.4
resolution: "reusify@npm:1.0.4"
@@ -16646,6 +20055,26 @@ __metadata:
languageName: unknown
linkType: soft
+"router@npm:^2.2.0":
+ version: 2.2.0
+ resolution: "router@npm:2.2.0"
+ dependencies:
+ debug: "npm:^4.4.0"
+ depd: "npm:^2.0.0"
+ is-promise: "npm:^4.0.0"
+ parseurl: "npm:^1.3.3"
+ path-to-regexp: "npm:^8.0.0"
+ checksum: 10c0/3279de7450c8eae2f6e095e9edacbdeec0abb5cb7249c6e719faa0db2dba43574b4fff5892d9220631c9abaff52dd3cad648cfea2aaace845e1a071915ac8867
+ languageName: node
+ linkType: hard
+
+"run-applescript@npm:^7.0.0":
+ version: 7.1.0
+ resolution: "run-applescript@npm:7.1.0"
+ checksum: 10c0/ab826c57c20f244b2ee807704b1ef4ba7f566aa766481ae5922aac785e2570809e297c69afcccc3593095b538a8a77d26f2b2e9a1d9dffee24e0e039502d1a03
+ languageName: node
+ linkType: hard
+
"run-parallel@npm:^1.1.9":
version: 1.2.0
resolution: "run-parallel@npm:1.2.0"
@@ -16854,6 +20283,25 @@ __metadata:
languageName: node
linkType: hard
+"send@npm:^1.1.0, send@npm:^1.2.0":
+ version: 1.2.1
+ resolution: "send@npm:1.2.1"
+ dependencies:
+ debug: "npm:^4.4.3"
+ encodeurl: "npm:^2.0.0"
+ escape-html: "npm:^1.0.3"
+ etag: "npm:^1.8.1"
+ fresh: "npm:^2.0.0"
+ http-errors: "npm:^2.0.1"
+ mime-types: "npm:^3.0.2"
+ ms: "npm:^2.1.3"
+ on-finished: "npm:^2.4.1"
+ range-parser: "npm:^1.2.1"
+ statuses: "npm:^2.0.2"
+ checksum: 10c0/fbbbbdc902a913d65605274be23f3d604065cfc3ee3d78bf9fc8af1dc9fc82667c50d3d657f5e601ac657bac9b396b50ee97bd29cd55436320cf1cddebdcec72
+ languageName: node
+ linkType: hard
+
"sentence-case@npm:^2.1.0":
version: 2.1.1
resolution: "sentence-case@npm:2.1.1"
@@ -16894,6 +20342,18 @@ __metadata:
languageName: node
linkType: hard
+"serve-static@npm:^2.2.0":
+ version: 2.2.1
+ resolution: "serve-static@npm:2.2.1"
+ dependencies:
+ encodeurl: "npm:^2.0.0"
+ escape-html: "npm:^1.0.3"
+ parseurl: "npm:^1.3.3"
+ send: "npm:^1.2.0"
+ checksum: 10c0/37986096e8572e2dfaad35a3925fa8da0c0969f8814fd7788e84d4d388bc068cf0c06d1658509788e55bed942a6b6d040a8a267fa92bb9ffb1179f8bacde5fd7
+ languageName: node
+ linkType: hard
+
"servify@npm:^0.1.12":
version: 0.1.12
resolution: "servify@npm:0.1.12"
@@ -16954,7 +20414,7 @@ __metadata:
languageName: node
linkType: hard
-"setprototypeof@npm:1.2.0":
+"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0":
version: 1.2.0
resolution: "setprototypeof@npm:1.2.0"
checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc
@@ -16983,6 +20443,50 @@ __metadata:
languageName: node
linkType: hard
+"shadcn@npm:^4.0.8":
+ version: 4.0.8
+ resolution: "shadcn@npm:4.0.8"
+ dependencies:
+ "@babel/core": "npm:^7.28.0"
+ "@babel/parser": "npm:^7.28.0"
+ "@babel/plugin-transform-typescript": "npm:^7.28.0"
+ "@babel/preset-typescript": "npm:^7.27.1"
+ "@dotenvx/dotenvx": "npm:^1.48.4"
+ "@modelcontextprotocol/sdk": "npm:^1.26.0"
+ "@types/validate-npm-package-name": "npm:^4.0.2"
+ browserslist: "npm:^4.26.2"
+ commander: "npm:^14.0.0"
+ cosmiconfig: "npm:^9.0.0"
+ dedent: "npm:^1.6.0"
+ deepmerge: "npm:^4.3.1"
+ diff: "npm:^8.0.2"
+ execa: "npm:^9.6.0"
+ fast-glob: "npm:^3.3.3"
+ fs-extra: "npm:^11.3.1"
+ fuzzysort: "npm:^3.1.0"
+ https-proxy-agent: "npm:^7.0.6"
+ kleur: "npm:^4.1.5"
+ msw: "npm:^2.10.4"
+ node-fetch: "npm:^3.3.2"
+ open: "npm:^11.0.0"
+ ora: "npm:^8.2.0"
+ postcss: "npm:^8.5.6"
+ postcss-selector-parser: "npm:^7.1.0"
+ prompts: "npm:^2.4.2"
+ recast: "npm:^0.23.11"
+ stringify-object: "npm:^5.0.0"
+ tailwind-merge: "npm:^3.0.1"
+ ts-morph: "npm:^26.0.0"
+ tsconfig-paths: "npm:^4.2.0"
+ validate-npm-package-name: "npm:^7.0.1"
+ zod: "npm:^3.24.1"
+ zod-to-json-schema: "npm:^3.24.6"
+ bin:
+ shadcn: dist/index.js
+ checksum: 10c0/9b877204f6378698460505b24812d293b2fbb01aeada1751748a66c53baeacbbad3bf0be82ee94cd267f17100e50329d46068588f7a167fac73a20f339577d43
+ languageName: node
+ linkType: hard
+
"shallowequal@npm:^1.0.2":
version: 1.1.0
resolution: "shallowequal@npm:1.1.0"
@@ -17006,6 +20510,41 @@ __metadata:
languageName: node
linkType: hard
+"side-channel-list@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "side-channel-list@npm:1.0.0"
+ dependencies:
+ es-errors: "npm:^1.3.0"
+ object-inspect: "npm:^1.13.3"
+ checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d
+ languageName: node
+ linkType: hard
+
+"side-channel-map@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "side-channel-map@npm:1.0.1"
+ dependencies:
+ call-bound: "npm:^1.0.2"
+ es-errors: "npm:^1.3.0"
+ get-intrinsic: "npm:^1.2.5"
+ object-inspect: "npm:^1.13.3"
+ checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672
+ languageName: node
+ linkType: hard
+
+"side-channel-weakmap@npm:^1.0.2":
+ version: 1.0.2
+ resolution: "side-channel-weakmap@npm:1.0.2"
+ dependencies:
+ call-bound: "npm:^1.0.2"
+ es-errors: "npm:^1.3.0"
+ get-intrinsic: "npm:^1.2.5"
+ object-inspect: "npm:^1.13.3"
+ side-channel-map: "npm:^1.0.1"
+ checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185
+ languageName: node
+ linkType: hard
+
"side-channel@npm:^1.0.4":
version: 1.0.4
resolution: "side-channel@npm:1.0.4"
@@ -17017,14 +20556,27 @@ __metadata:
languageName: node
linkType: hard
-"signal-exit@npm:^3.0.2":
+"side-channel@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "side-channel@npm:1.1.0"
+ dependencies:
+ es-errors: "npm:^1.3.0"
+ object-inspect: "npm:^1.13.3"
+ side-channel-list: "npm:^1.0.0"
+ side-channel-map: "npm:^1.0.1"
+ side-channel-weakmap: "npm:^1.0.2"
+ checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6
+ languageName: node
+ linkType: hard
+
+"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3":
version: 3.0.7
resolution: "signal-exit@npm:3.0.7"
checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912
languageName: node
linkType: hard
-"signal-exit@npm:^4.0.1":
+"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0":
version: 4.1.0
resolution: "signal-exit@npm:4.1.0"
checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83
@@ -17088,6 +20640,13 @@ __metadata:
languageName: node
linkType: hard
+"sisteransi@npm:^1.0.5":
+ version: 1.0.5
+ resolution: "sisteransi@npm:1.0.5"
+ checksum: 10c0/230ac975cca485b7f6fe2b96a711aa62a6a26ead3e6fb8ba17c5a00d61b8bed0d7adc21f5626b70d7c33c62ff4e63933017a6462942c719d1980bb0b1207ad46
+ languageName: node
+ linkType: hard
+
"smart-buffer@npm:^4.2.0":
version: 4.2.0
resolution: "smart-buffer@npm:4.2.0"
@@ -17196,6 +20755,16 @@ __metadata:
languageName: node
linkType: hard
+"sonner@npm:^2.0.7":
+ version: 2.0.7
+ resolution: "sonner@npm:2.0.7"
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ checksum: 10c0/6966ab5e892ed6aab579a175e4a24f3b48747f0fc21cb68c3e33cb41caa7a0eebeb098c210545395e47a18d585eb8734ae7dd12d2bd18c8a3294a1ee73f997d9
+ languageName: node
+ linkType: hard
+
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.2":
version: 1.0.2
resolution: "source-map-js@npm:1.0.2"
@@ -17217,6 +20786,13 @@ __metadata:
languageName: node
linkType: hard
+"source-map@npm:~0.6.1":
+ version: 0.6.1
+ resolution: "source-map@npm:0.6.1"
+ checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011
+ languageName: node
+ linkType: hard
+
"space-separated-tokens@npm:^2.0.0":
version: 2.0.2
resolution: "space-separated-tokens@npm:2.0.2"
@@ -17375,6 +20951,20 @@ __metadata:
languageName: node
linkType: hard
+"statuses@npm:^2.0.1, statuses@npm:^2.0.2, statuses@npm:~2.0.2":
+ version: 2.0.2
+ resolution: "statuses@npm:2.0.2"
+ checksum: 10c0/a9947d98ad60d01f6b26727570f3bcceb6c8fa789da64fe6889908fe2e294d57503b14bf2b5af7605c2d36647259e856635cd4c49eab41667658ec9d0080ec3f
+ languageName: node
+ linkType: hard
+
+"stdin-discarder@npm:^0.2.2":
+ version: 0.2.2
+ resolution: "stdin-discarder@npm:0.2.2"
+ checksum: 10c0/c78375e82e956d7a64be6e63c809c7f058f5303efcaf62ea48350af072bacdb99c06cba39209b45a071c1acbd49116af30df1df9abb448df78a6005b72f10537
+ languageName: node
+ linkType: hard
+
"stealthy-require@npm:^1.1.1":
version: 1.1.1
resolution: "stealthy-require@npm:1.1.1"
@@ -17430,6 +21020,13 @@ __metadata:
languageName: node
linkType: hard
+"strict-event-emitter@npm:^0.5.1":
+ version: 0.5.1
+ resolution: "strict-event-emitter@npm:0.5.1"
+ checksum: 10c0/f5228a6e6b6393c57f52f62e673cfe3be3294b35d6f7842fc24b172ae0a6e6c209fa83241d0e433fc267c503bc2f4ffdbe41a9990ff8ffd5ac425ec0489417f7
+ languageName: node
+ linkType: hard
+
"strict-uri-encode@npm:^1.0.0":
version: 1.1.0
resolution: "strict-uri-encode@npm:1.1.0"
@@ -17470,6 +21067,17 @@ __metadata:
languageName: node
linkType: hard
+"string-width@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "string-width@npm:7.2.0"
+ dependencies:
+ emoji-regex: "npm:^10.3.0"
+ get-east-asian-width: "npm:^1.0.0"
+ strip-ansi: "npm:^7.1.0"
+ checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9
+ languageName: node
+ linkType: hard
+
"string.prototype.trim@npm:^1.2.7":
version: 1.2.7
resolution: "string.prototype.trim@npm:1.2.7"
@@ -17528,6 +21136,17 @@ __metadata:
languageName: node
linkType: hard
+"stringify-object@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "stringify-object@npm:5.0.0"
+ dependencies:
+ get-own-enumerable-keys: "npm:^1.0.0"
+ is-obj: "npm:^3.0.0"
+ is-regexp: "npm:^3.1.0"
+ checksum: 10c0/f955bb0b41edb0a200bf5ba24d516a2d409c749a01224e14a088ecf07fec3d930ec90da3a681f6798b9d6a1b187cb3bb57f0d17525190006ef3bd609d0300bb9
+ languageName: node
+ linkType: hard
+
"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
version: 6.0.1
resolution: "strip-ansi@npm:6.0.1"
@@ -17555,6 +21174,15 @@ __metadata:
languageName: node
linkType: hard
+"strip-ansi@npm:^7.1.0":
+ version: 7.2.0
+ resolution: "strip-ansi@npm:7.2.0"
+ dependencies:
+ ansi-regex: "npm:^6.2.2"
+ checksum: 10c0/544d13b7582f8254811ea97db202f519e189e59d35740c46095897e254e4f1aa9fe1524a83ad6bc5ad67d4dd6c0281d2e0219ed62b880a6238a16a17d375f221
+ languageName: node
+ linkType: hard
+
"strip-bom@npm:^2.0.0":
version: 2.0.0
resolution: "strip-bom@npm:2.0.0"
@@ -17571,6 +21199,20 @@ __metadata:
languageName: node
linkType: hard
+"strip-final-newline@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "strip-final-newline@npm:2.0.0"
+ checksum: 10c0/bddf8ccd47acd85c0e09ad7375409d81653f645fda13227a9d459642277c253d877b68f2e5e4d819fe75733b0e626bac7e954c04f3236f6d196f79c94fa4a96f
+ languageName: node
+ linkType: hard
+
+"strip-final-newline@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "strip-final-newline@npm:4.0.0"
+ checksum: 10c0/b0cf2b62d597a1b0e3ebc42b88767f0a0d45601f89fd379a928a1812c8779440c81abba708082c946445af1d6b62d5f16e2a7cf4f30d9d6587b89425fae801ff
+ languageName: node
+ linkType: hard
+
"strip-hex-prefix@npm:1.0.0":
version: 1.0.0
resolution: "strip-hex-prefix@npm:1.0.0"
@@ -17753,6 +21395,27 @@ __metadata:
languageName: node
linkType: hard
+"tagged-tag@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "tagged-tag@npm:1.0.0"
+ checksum: 10c0/91d25c9ffb86a91f20522cefb2cbec9b64caa1febe27ad0df52f08993ff60888022d771e868e6416cf2e72dab68449d2139e8709ba009b74c6c7ecd4000048d1
+ languageName: node
+ linkType: hard
+
+"tailwind-merge@npm:^3.0.1, tailwind-merge@npm:^3.5.0":
+ version: 3.5.0
+ resolution: "tailwind-merge@npm:3.5.0"
+ checksum: 10c0/4dc588f5b5296ba3f38e1ebb41f02b6d24a8c5bb45e44b33748c118fb4b5767dd0efc464431ca3e75404056b618b5f67bec3708158baa65fed8a2fc9201e0c53
+ languageName: node
+ linkType: hard
+
+"tailwindcss@npm:4.2.1, tailwindcss@npm:^4.2.1":
+ version: 4.2.1
+ resolution: "tailwindcss@npm:4.2.1"
+ checksum: 10c0/482d734b582e9da509042ff59c1d7564d99e39e238c50ae907c20fa56177a8a00c3902f6971329971bd6a1c5357026ac76a849b8f2c69c94f0f59be99530ba54
+ languageName: node
+ linkType: hard
+
"tapable@npm:^2.2.0":
version: 2.2.1
resolution: "tapable@npm:2.2.1"
@@ -17760,6 +21423,13 @@ __metadata:
languageName: node
linkType: hard
+"tapable@npm:^2.3.0":
+ version: 2.3.0
+ resolution: "tapable@npm:2.3.0"
+ checksum: 10c0/cb9d67cc2c6a74dedc812ef3085d9d681edd2c1fa18e4aef57a3c0605fdbe44e6b8ea00bd9ef21bc74dd45314e39d31227aa031ebf2f5e38164df514136f2681
+ languageName: node
+ linkType: hard
+
"tar-fs@npm:~2.0.1":
version: 2.0.1
resolution: "tar-fs@npm:2.0.1"
@@ -17923,6 +21593,13 @@ __metadata:
languageName: node
linkType: hard
+"tiny-invariant@npm:^1.3.3":
+ version: 1.3.3
+ resolution: "tiny-invariant@npm:1.3.3"
+ checksum: 10c0/65af4a07324b591a059b35269cd696aba21bef2107f29b9f5894d83cc143159a204b299553435b03874ebb5b94d019afa8b8eff241c8a4cfee95872c2e1c1c4a
+ languageName: node
+ linkType: hard
+
"tiny-typed-emitter@npm:^2.1.0":
version: 2.1.0
resolution: "tiny-typed-emitter@npm:2.1.0"
@@ -17940,6 +21617,24 @@ __metadata:
languageName: node
linkType: hard
+"tldts-core@npm:^7.0.26":
+ version: 7.0.26
+ resolution: "tldts-core@npm:7.0.26"
+ checksum: 10c0/3130c4a338bc5fa2463cec199b1b7b610e7dc570e33505c66fcf223ebea76f8e316b4531d2de1eb868488b9f02a92cd8aa610334d0fa3bf97f46bf89e438f856
+ languageName: node
+ linkType: hard
+
+"tldts@npm:^7.0.5":
+ version: 7.0.26
+ resolution: "tldts@npm:7.0.26"
+ dependencies:
+ tldts-core: "npm:^7.0.26"
+ bin:
+ tldts: bin/cli.js
+ checksum: 10c0/de7df3cfb03517afa09c3f3ad08d5e68d1735d5a65af83d978a4032a5784bbd4171f3d4b029575981f9a3045620152b55fb3c24445d76f7735a5c18f3b0d9c69
+ languageName: node
+ linkType: hard
+
"to-arraybuffer@npm:^1.0.0":
version: 1.0.1
resolution: "to-arraybuffer@npm:1.0.1"
@@ -17974,7 +21669,7 @@ __metadata:
languageName: node
linkType: hard
-"toidentifier@npm:1.0.1":
+"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1":
version: 1.0.1
resolution: "toidentifier@npm:1.0.1"
checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1
@@ -18004,6 +21699,15 @@ __metadata:
languageName: node
linkType: hard
+"tough-cookie@npm:^6.0.0":
+ version: 6.0.1
+ resolution: "tough-cookie@npm:6.0.1"
+ dependencies:
+ tldts: "npm:^7.0.5"
+ checksum: 10c0/ec70bd6b1215efe4ed31a158f0be3e4c9088fcbd8620edc23a5860d4f3d85c757b77e274baaa700f7b25e409f4181552ed189603c2b2e1a9f88104da3a61a37d
+ languageName: node
+ linkType: hard
+
"tough-cookie@npm:~2.5.0":
version: 2.5.0
resolution: "tough-cookie@npm:2.5.0"
@@ -18080,6 +21784,16 @@ __metadata:
languageName: node
linkType: hard
+"ts-morph@npm:^26.0.0":
+ version: 26.0.0
+ resolution: "ts-morph@npm:26.0.0"
+ dependencies:
+ "@ts-morph/common": "npm:~0.27.0"
+ code-block-writer: "npm:^13.0.3"
+ checksum: 10c0/c6880d90a1eefe0ce6555bf8c11cc104b1f36f84bd36a37a82b9ae0b974f51fe6b1bc91bb0ec42550158dc1c812329d6433e1237cba64f1ef515c129b321dd5d
+ languageName: node
+ linkType: hard
+
"ts-node@npm:^10.9.1":
version: 10.9.1
resolution: "ts-node@npm:10.9.1"
@@ -18188,7 +21902,7 @@ __metadata:
languageName: node
linkType: hard
-"tslib@npm:^2.0.3, tslib@npm:^2.8.0":
+"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@@ -18248,6 +21962,13 @@ __metadata:
languageName: node
linkType: hard
+"tw-animate-css@npm:^1.4.0":
+ version: 1.4.0
+ resolution: "tw-animate-css@npm:1.4.0"
+ checksum: 10c0/6cfbc19ccc73883ec80ef1f9147f43e736cb01ee99c8172968b37eb81b720523d30e38b1a96aef92db3c586d864204db5510b51744ddacbbf0ad8e3c7fb56ec7
+ languageName: node
+ linkType: hard
+
"tweetnacl@npm:^0.14.3, tweetnacl@npm:~0.14.0":
version: 0.14.5
resolution: "tweetnacl@npm:0.14.5"
@@ -18271,6 +21992,26 @@ __metadata:
languageName: node
linkType: hard
+"type-fest@npm:^5.2.0":
+ version: 5.4.4
+ resolution: "type-fest@npm:5.4.4"
+ dependencies:
+ tagged-tag: "npm:^1.0.0"
+ checksum: 10c0/bf9c6d7df5383fd720aac71da8ce8690ff1c554459d19cf3c72d61eac98255dba57abe20c628f91f4116f66211791462fdafa90b2be2d7405a5a4c295e4d849d
+ languageName: node
+ linkType: hard
+
+"type-is@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "type-is@npm:2.0.1"
+ dependencies:
+ content-type: "npm:^1.0.5"
+ media-typer: "npm:^1.1.0"
+ mime-types: "npm:^3.0.0"
+ checksum: 10c0/7f7ec0a060b16880bdad36824ab37c26019454b67d73e8a465ed5a3587440fbe158bc765f0da68344498235c877e7dbbb1600beccc94628ed05599d667951b99
+ languageName: node
+ linkType: hard
+
"type-is@npm:~1.6.18":
version: 1.6.18
resolution: "type-is@npm:1.6.18"
@@ -18621,6 +22362,13 @@ __metadata:
languageName: node
linkType: hard
+"unicorn-magic@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "unicorn-magic@npm:0.3.0"
+ checksum: 10c0/0a32a997d6c15f1c2a077a15b1c4ca6f268d574cf5b8975e778bb98e6f8db4ef4e86dfcae4e158cd4c7e38fb4dd383b93b13eefddc7f178dea13d3ac8a603271
+ languageName: node
+ linkType: hard
+
"unified@npm:^10.0.0":
version: 10.1.2
resolution: "unified@npm:10.1.2"
@@ -18737,6 +22485,13 @@ __metadata:
languageName: node
linkType: hard
+"until-async@npm:^3.0.2":
+ version: 3.0.2
+ resolution: "until-async@npm:3.0.2"
+ checksum: 10c0/61c8b03895dbe18fe3d90316d0a1894e0c131ea4b1673f6ce78eed993d0bb81bbf4b7adf8477e9ff7725782a76767eed9d077561cfc9f89b4a1ebe61f7c9828e
+ languageName: node
+ linkType: hard
+
"update-browserslist-db@npm:^1.0.16":
version: 1.0.16
resolution: "update-browserslist-db@npm:1.0.16"
@@ -18817,6 +22572,46 @@ __metadata:
languageName: node
linkType: hard
+"use-callback-ref@npm:^1.3.3":
+ version: 1.3.3
+ resolution: "use-callback-ref@npm:1.3.3"
+ dependencies:
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/f887488c6e6075cdad4962979da1714b217bcb1ee009a9e57ce9a844bcfc4c3a99e93983dfc2e5af9e0913824d24e730090ff255e902c516dcb58d2d3837e01c
+ languageName: node
+ linkType: hard
+
+"use-sidecar@npm:^1.1.3":
+ version: 1.1.3
+ resolution: "use-sidecar@npm:1.1.3"
+ dependencies:
+ detect-node-es: "npm:^1.1.0"
+ tslib: "npm:^2.0.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/161599bf921cfaa41c85d2b01c871975ee99260f3e874c2d41c05890d41170297bdcf314bc5185e7a700de2034ac5b888e3efc8e9f35724f4918f53538d717c9
+ languageName: node
+ linkType: hard
+
+"use-sync-external-store@npm:^1.5.0":
+ version: 1.6.0
+ resolution: "use-sync-external-store@npm:1.6.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b
+ languageName: node
+ linkType: hard
+
"utf-8-validate@npm:5.0.7":
version: 5.0.7
resolution: "utf-8-validate@npm:5.0.7"
@@ -18854,7 +22649,7 @@ __metadata:
languageName: node
linkType: hard
-"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1":
+"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942
@@ -18964,6 +22759,13 @@ __metadata:
languageName: node
linkType: hard
+"validate-npm-package-name@npm:^7.0.1":
+ version: 7.0.2
+ resolution: "validate-npm-package-name@npm:7.0.2"
+ checksum: 10c0/adf32e943148e13e8df13d06b855493908e6ae7a847610e8543c6291cbf42f40e653249a5b2275e2e615e3224c574ade5a9064a9e2d1ab629386284ea99e8f39
+ languageName: node
+ linkType: hard
+
"value-or-promise@npm:1.0.11":
version: 1.0.11
resolution: "value-or-promise@npm:1.0.11"
@@ -18992,13 +22794,25 @@ __metadata:
languageName: node
linkType: hard
-"vary@npm:^1, vary@npm:~1.1.2":
+"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2":
version: 1.1.2
resolution: "vary@npm:1.1.2"
checksum: 10c0/f15d588d79f3675135ba783c91a4083dcd290a2a5be9fcb6514220a1634e23df116847b1cc51f66bfb0644cf9353b2abb7815ae499bab06e46dd33c1a6bf1f4f
languageName: node
linkType: hard
+"vaul@npm:^1.1.2":
+ version: 1.1.2
+ resolution: "vaul@npm:1.1.2"
+ dependencies:
+ "@radix-ui/react-dialog": "npm:^1.1.1"
+ peerDependencies:
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+ checksum: 10c0/a6da539eb5576c0004a6b17e3673ea1db2c34e80355860131183abf53279ce025bbd016d542c345d1cc8464ad12f9dc9860949c751055d8a84961e8472a53707
+ languageName: node
+ linkType: hard
+
"verror@npm:1.10.0":
version: 1.10.0
resolution: "verror@npm:1.10.0"
@@ -19626,6 +23440,17 @@ __metadata:
languageName: node
linkType: hard
+"wrap-ansi@npm:^6.2.0":
+ version: 6.2.0
+ resolution: "wrap-ansi@npm:6.2.0"
+ dependencies:
+ ansi-styles: "npm:^4.0.0"
+ string-width: "npm:^4.1.0"
+ strip-ansi: "npm:^6.0.0"
+ checksum: 10c0/baad244e6e33335ea24e86e51868fe6823626e3a3c88d9a6674642afff1d34d9a154c917e74af8d845fd25d170c4ea9cf69a47133c3f3656e1252b3d462d9f6c
+ languageName: node
+ linkType: hard
+
"wrap-ansi@npm:^8.1.0":
version: 8.1.0
resolution: "wrap-ansi@npm:8.1.0"
@@ -19739,6 +23564,16 @@ __metadata:
languageName: node
linkType: hard
+"wsl-utils@npm:^0.3.0":
+ version: 0.3.1
+ resolution: "wsl-utils@npm:0.3.1"
+ dependencies:
+ is-wsl: "npm:^3.1.0"
+ powershell-utils: "npm:^0.1.0"
+ checksum: 10c0/b3ba99cc6b71f66457eef598d529beeb8cb57a72646877fe25993894b808c60b82f6d47df5463f0b6e54632272f62f5eaea105c12784fd09b06f500f3f53aa2e
+ languageName: node
+ linkType: hard
+
"xhr-request-promise@npm:^0.1.2":
version: 0.1.3
resolution: "xhr-request-promise@npm:0.1.3"
@@ -19917,7 +23752,7 @@ __metadata:
languageName: node
linkType: hard
-"yargs@npm:^17.1.1":
+"yargs@npm:^17.1.1, yargs@npm:^17.7.2":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
@@ -19967,3 +23802,40 @@ __metadata:
checksum: 10c0/dceb44c28578b31641e13695d200d34ec4ab3966a5729814d5445b194933c096b7ced71494ce53a0e8820685d1d010df8b2422e5bf2cdea7e469d97ffbea306f
languageName: node
linkType: hard
+
+"yoctocolors-cjs@npm:^2.1.3":
+ version: 2.1.3
+ resolution: "yoctocolors-cjs@npm:2.1.3"
+ checksum: 10c0/584168ef98eb5d913473a4858dce128803c4a6cd87c0f09e954fa01126a59a33ab9e513b633ad9ab953786ed16efdd8c8700097a51635aafaeed3fef7712fa79
+ languageName: node
+ linkType: hard
+
+"yoctocolors@npm:^2.1.1":
+ version: 2.1.2
+ resolution: "yoctocolors@npm:2.1.2"
+ checksum: 10c0/b220f30f53ebc2167330c3adc86a3c7f158bcba0236f6c67e25644c3188e2571a6014ffc1321943bb619460259d3d27eb4c9cc58c2d884c1b195805883ec7066
+ languageName: node
+ linkType: hard
+
+"zod-to-json-schema@npm:^3.24.6, zod-to-json-schema@npm:^3.25.1":
+ version: 3.25.1
+ resolution: "zod-to-json-schema@npm:3.25.1"
+ peerDependencies:
+ zod: ^3.25 || ^4
+ checksum: 10c0/711b30e34d1f1211f1afe64bf457f0d799234199dc005cca720b236ea808804c03164039c232f5df33c46f462023874015a8a0b3aab1585eca14124c324db7e2
+ languageName: node
+ linkType: hard
+
+"zod@npm:^3.24.1":
+ version: 3.25.76
+ resolution: "zod@npm:3.25.76"
+ checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c
+ languageName: node
+ linkType: hard
+
+"zod@npm:^3.25 || ^4.0":
+ version: 4.3.6
+ resolution: "zod@npm:4.3.6"
+ checksum: 10c0/860d25a81ab41d33aa25f8d0d07b091a04acb426e605f396227a796e9e800c44723ed96d0f53a512b57be3d1520f45bf69c0cb3b378a232a00787a2609625307
+ languageName: node
+ linkType: hard