diff --git a/README.md b/README.md index bc3c1b1..9fcfe26 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,129 @@ -# T.O.P. (Topology Orchestration Platform) +# TOPHAT + +## Installation + +### STEP 1 - Interconnects + +### Supported Platforms +Interconnect devices must meet the following requirements to ensure compatibility with TOPHAT: + +**Operating System (OS)** + + - Cisco IOSv + - Cisco IOS + - Cisco IOS-XE + +**Port Density** + +TOPHAT can support up to 2 Interconnect devices, each with varying port densities: + + - **1x Interconnects** + - Must be 48 ports + - **2x Interconnects** + - Interconnect 1: 48 ports + - Interconnect 2: 24-48 ports + +--- + +#### Initial Configuration + +Interconnect devices must be **remotely accessible** via **SSH** from the **out-of-band (OOB) management interface** to the TOPHAT application host. + +It is recommended to use spanning-tree mode MST, and static assign OOB IP addresses for the Interconnects. + +--- + +##### Authentication + +- **SSH Access**: Required for secure remote administration. +- **User Authentication**: Devices must support **username/password authentication**. +- **Privilege Escalation**: An **enable secret password** must be configured for administrative access. + +An example basic configuration is provided below: + +``` +hostname Interconnect +ip domain-name interconnect.lab +username admin privilege 15 secret 0 cisco +enable secret cisco +line vty 0 15 + login local + transport input ssh +crypto key generate rsa modulus 2048 +ip ssh version 2 +``` + +--- + +##### Lab Device Ports + +All device interfaces (**excluding the last four ports**) must be configured for **dot1q tunneling (QinQ)** to encapsulate Layer 2 protocol frames. These ports serve as direct connections to lab devices. + +``` +interface range GigabitEthernet1/0/1-44 + shutdown + no switchport access vlan + switchport mode dot1q-tunnel + negotiation auto + mtu 9000 + mtu 8978 + l2protocol-tunnel cdp + l2protocol-tunnel lldp + l2protocol-tunnel stp + l2protocol-tunnel vtp + l2protocol-tunnel point-to-point pagp + l2protocol-tunnel point-to-point lacp + l2protocol-tunnel point-to-point udld + no cdp enable +``` + +--- + +##### Transport Ports + +The last four interfaces (**45-48** or **21-24**, depending on the platform) are dedicated to **transporting traffic between Interconnects**. + +*If you are only using one Interconnect, shut these ports.* + +``` +port-channel load-balance src-dst-mac +! +interface GigabitEthernet1/0/45-48 + channel-protocol lacp + channel-group 1 mode active + no shutdown +! +interface Port-channel1 + switchport mode trunk + switchport trunk allowed vlan all + switchport nonegotiate + mtu 9000 + mtu 8978 + no cdp enable + no shutdown +``` + +### STEP 2 - Application + +#### Environment Setup + +Once the Interconnects are configured, proceed with the installation of the TOPHAT application. + +Navigate to the [TOPHAT GitHub](https://github.com/breyr/TOPHAT/blob/main/compose.prod.yaml) repository, and make a copy of the `compose.prod.yaml` file. + +Save the file as `docker-compose.yml` in your desired directory. + +#### Running the Application + +Run the following command to start the application: + +```sh +docker-compose -f docker-compose.yml up -d +``` + +TOPHAT will now be running at [0.0.0.0:80](0.0.0.0:80). + +To expose TOPHAT outside of your LAN, we recommend using [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) to securely expose the UI externally with ZeroTrust. ## Developing diff --git a/backend/src/controllers/DeviceController.ts b/backend/src/controllers/DeviceController.ts index ca51445..e572438 100644 --- a/backend/src/controllers/DeviceController.ts +++ b/backend/src/controllers/DeviceController.ts @@ -162,7 +162,7 @@ export class DeviceController { try { const { id } = req.params; // will have a payload user id otherwise this request is not authenticated through middleware - const device = await this.deviceService.unbookDevice(parseInt(id), req.jwt_payload?.id!); + const device = await this.deviceService.unbookDevice(parseInt(id), req.jwt_payload?.id!, req.jwt_payload?.accountType!); // we successfully unbooked the device if (device) { diff --git a/backend/src/repositories/PrismaDeviceRepository.ts b/backend/src/repositories/PrismaDeviceRepository.ts index a119bac..d96e198 100644 --- a/backend/src/repositories/PrismaDeviceRepository.ts +++ b/backend/src/repositories/PrismaDeviceRepository.ts @@ -1,4 +1,4 @@ -import { DeviceType, IconType, PrismaClient, type Device } from "@prisma/client"; +import { AccountType, DeviceType, IconType, PrismaClient, type Device } from "@prisma/client"; import bcrypt from 'bcryptjs'; import { IDeviceRepository } from "../types/classInterfaces"; @@ -108,7 +108,7 @@ export class PrismaDeviceRepository implements IDeviceRepository { }); } - async unbookDevice(deviceId: number, userId: number): Promise { + async unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise { return await this.prisma.$transaction(async (tx) => { // check if device is already booked const current = await tx.device.findUnique({ @@ -116,8 +116,8 @@ export class PrismaDeviceRepository implements IDeviceRepository { select: { userId: true } }); - // only allow unbooking of device if userIds match - if (current?.userId !== userId) { + // only allow unbooking of device if userIds match AND account type is not admin or owner + if (accountType !== 'ADMIN' && accountType !== 'OWNER' && current?.userId !== userId) { throw new Error("UNAUTHORIZED"); } diff --git a/backend/src/services/DeviceService.ts b/backend/src/services/DeviceService.ts index e6be06b..83248e7 100644 --- a/backend/src/services/DeviceService.ts +++ b/backend/src/services/DeviceService.ts @@ -1,4 +1,4 @@ -import type { Device, DeviceType, IconType } from "@prisma/client"; +import type { AccountType, Device, DeviceType, IconType } from "@prisma/client"; import { IDeviceRepository, IDeviceService } from "../types/classInterfaces"; export class DeviceService implements IDeviceService { @@ -58,7 +58,7 @@ export class DeviceService implements IDeviceService { return this.deviceRepository.bookDevice(deviceId, userId); } - async unbookDevice(deviceId: number, userId: number): Promise { - return this.deviceRepository.unbookDevice(deviceId, userId); + async unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise { + return this.deviceRepository.unbookDevice(deviceId, userId, accountType); } } \ No newline at end of file diff --git a/backend/src/types/classInterfaces.ts b/backend/src/types/classInterfaces.ts index 3ef05ef..de4b6a4 100644 --- a/backend/src/types/classInterfaces.ts +++ b/backend/src/types/classInterfaces.ts @@ -1,5 +1,5 @@ // holds interfaces similar to C# -import { AppConfig, AppUser, Connection, Device, DeviceType, IconType, Topology } from "@prisma/client"; +import { AccountType, AppConfig, AppUser, Connection, Device, DeviceType, IconType, Topology } from "@prisma/client"; import type { CreateConnectionRequestPayload, CreateTopologyRequestPayload, RegisterUserRequestPayload } from "common"; import { UpdateTopologyDTO } from "./types"; @@ -55,7 +55,7 @@ export interface IDeviceRepository { findByType(deviceType: DeviceType): Promise; findByIcon(deviceIcon: IconType): Promise; bookDevice(deviceId: number, userId: number): Promise; - unbookDevice(deviceId: number, userId: number): Promise; + unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise; } export interface IDeviceService { @@ -69,7 +69,7 @@ export interface IDeviceService { getDevicesByType(deviceType: DeviceType): Promise; getDevicesByIcon(deviceIcon: IconType): Promise; bookDevice(deviceId: number, userId: number): Promise; - unbookDevice(deviceId: number, userId: number): Promise; + unbookDevice(deviceId: number, userId: number, accountType: AccountType): Promise; } export interface IConnectionRepository { diff --git a/compose.prod.yaml b/compose.prod.yaml index 7a69e07..59b9aeb 100644 --- a/compose.prod.yaml +++ b/compose.prod.yaml @@ -14,7 +14,7 @@ services: - postgres_data:/var/lib/postgresql/data backend: - image: breyr/top-backend:1.0.0 + image: breyr/top-backend:1.0.1 container_name: backend environment: DATABASE_URL: postgres://demo:demo@postgres:5432/demo @@ -25,7 +25,7 @@ services: - postgres frontend: - image: breyr/top-frontend:1.0.0 + image: breyr/top-frontend:1.0.1 container_name: frontend ports: - "80:80" @@ -33,7 +33,7 @@ services: - backend interconnect-api: - image: breyr/top-interconnectapi:1.0.0 + image: breyr/top-interconnectapi:1.0.1 container_name: interconnect-api environment: SECRET_KEY: your_secret diff --git a/frontend/package.json b/frontend/package.json index b4046f9..2fcc379 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,7 +40,7 @@ "tailwindcss": "^3.4.14", "typescript": "^5.5.3", "typescript-eslint": "^8.7.0", - "vite": "^6.1.1", + "vite": "^6.2.4", "vite-plugin-svgr": "^4.3.0" } } diff --git a/frontend/src/components/TopologyCard.tsx b/frontend/src/components/TopologyCard.tsx index aa941c9..943b543 100644 --- a/frontend/src/components/TopologyCard.tsx +++ b/frontend/src/components/TopologyCard.tsx @@ -1,5 +1,5 @@ import { Topology } from 'common'; -import { Image, Trash } from "lucide-react"; +import { Archive, Image, Trash } from "lucide-react"; import React, { useEffect, useState } from 'react'; import { useNavigate } from "react-router-dom"; import { useAuth } from "../hooks/useAuth.ts"; @@ -21,7 +21,6 @@ const TopologyCard: React.FC = ({ onDelete, onArchive, readOnly, - userId }) => { const { menuOpen, hideMenu } = useContextMenu(); const { user } = useAuth(); @@ -29,7 +28,6 @@ const TopologyCard: React.FC = ({ const [thumbnailSrc, setThumbnailSrc] = useState(null); const [archived, setArchived] = useState(initialArchived); const [isModalOpen, setIsModalOpen] = useState(false); - const ownsTopology = user?.id === userId; const handleClick = (event: React.MouseEvent) => { if (archived) return; @@ -87,9 +85,9 @@ const TopologyCard: React.FC = ({ return (
= ({

{archived ? "Archived" : "Active"} diff --git a/frontend/src/components/TopologyDetailCard.tsx b/frontend/src/components/TopologyDetailCard.tsx index 80be032..68e5802 100644 --- a/frontend/src/components/TopologyDetailCard.tsx +++ b/frontend/src/components/TopologyDetailCard.tsx @@ -1,7 +1,6 @@ import { Topology } from 'common'; -import { Trash } from "lucide-react"; -import React, { useState } from 'react'; -import { useAuth } from "../hooks/useAuth.ts"; +import { Image, Trash } from "lucide-react"; +import React, { useEffect, useState } from 'react'; import DeletionModal from "./DeletionModal.tsx"; interface TopologyProps extends Topology { @@ -13,28 +12,18 @@ interface TopologyProps extends Topology { const TopologyDetailCard: React.FC = ({ id, name, - archived: initialArchived, + archived, updatedAt, onDelete, - onArchive, - userId, + thumbnail, reactFlowState }) => { - const { user } = useAuth(); - const [archived, setArchived] = useState(initialArchived); + const [thumbnailSrc, setThumbnailSrc] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); - const ownsTopology = user?.id === userId; // get the devices on the topology const devicesUsedInTopology = reactFlowState?.nodes.map(n => n.id); - // toggle the archive state - const toggleArchived = (event: React.MouseEvent) => { - event.stopPropagation(); - setArchived(!archived); - onArchive(id); - }; - const handleDeleteClick = (e: React.MouseEvent) => { e.stopPropagation(); setIsModalOpen(true); @@ -45,6 +34,30 @@ const TopologyDetailCard: React.FC = ({ setIsModalOpen(false); }; + // handle conversion of thumbnail + useEffect(() => { + if (!thumbnail) return; + try { + // convert the object to a Uint8Array + const byteArray = new Uint8Array(Object.values(thumbnail)); + + // convert Uint8Array to binary string + const binaryString = Array.from(byteArray) + .map(byte => String.fromCharCode(byte)) + .join(''); + + // convert binary string to base64 + const base64String = btoa(binaryString); + // check if empty + if (base64String !== "AA==") { + const thumbnailSourceString = `data:image/jpg;base64,${base64String}`; + setThumbnailSrc(thumbnailSourceString); + } + } catch (error) { + console.error('Error converting to base64:', error); + } + }, [thumbnail]); + return (
diff --git a/frontend/src/components/reactflow/TopologyCanvas.tsx b/frontend/src/components/reactflow/TopologyCanvas.tsx index 7425ec6..68c3e1b 100644 --- a/frontend/src/components/reactflow/TopologyCanvas.tsx +++ b/frontend/src/components/reactflow/TopologyCanvas.tsx @@ -329,6 +329,7 @@ const TopologyCanvas = () => { selectionMode={SelectionMode.Partial} onPaneClick={onPaneClick} onNodeContextMenu={onNodeContextMenu} + snapToGrid={true} > {menu && ( diff --git a/frontend/src/components/reactflow/overlayui/ContextMenu.tsx b/frontend/src/components/reactflow/overlayui/ContextMenu.tsx index 731eb62..71c72a2 100644 --- a/frontend/src/components/reactflow/overlayui/ContextMenu.tsx +++ b/frontend/src/components/reactflow/overlayui/ContextMenu.tsx @@ -84,11 +84,17 @@ export default function ContextMenu({ onClick(); // close the context menu setDisableDelete(true); // Check if the device has any edges - const edgesForDevice = edges.filter(e => e.source === node?.data.deviceData?.name || e.target === node?.data.deviceData?.name); - console.log(edgesForDevice); + const edgesForDevice = edges.filter(e => e.source === node?.data.deviceData?.name || e.target === node?.data.deviceData?.name).map(e => ({ + value: e.id, + label: `(${e.source}) ${e.data?.sourcePort ?? ''} -> (${e.target}) ${e.data?.targetPort ?? ''}`, + firstLabDevice: e.source, + firstLabDevicePort: e.data?.sourcePort ?? '', + secondLabDevice: e.target, + secondLabDevicePort: e.data?.targetPort ?? '', + })); if (edgesForDevice.length > 0) { // attempt to delete all links - const numFailures = await deleteLinkBulk(new Set(currentEdges)); + const numFailures = await deleteLinkBulk(new Set(edgesForDevice)); // only remove the node if all links were successfully deleted if (numFailures === 0) { diff --git a/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx b/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx index 7de1fbe..ac92688 100644 --- a/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx +++ b/frontend/src/components/reactflow/overlayui/CreateLinkModal.tsx @@ -16,37 +16,24 @@ interface CreateLinkModalProps { export default function CreateLinkModal({ deviceData, currentDevicePorts, labDevices, onClose }: CreateLinkModalProps) { const { user } = useAuth(); - const { getEdges, getNodes } = useReactFlow, Edge>(); + const { getEdges } = useReactFlow, Edge>(); const { createLink } = useLinkOperations(); const [selectedFirstDevice, setSelectedFirstDevice] = useState(deviceData?.name ?? ""); const [selectedFirstDevicePort, setSelectedFirstDevicePort] = useState(""); const [selectedSecondDevice, setSelectedSecondDevice] = useState(""); const [selectedSecondDevicePort, setSelectedSecondDevicePort] = useState(""); const [availablePorts, setAvailablePorts] = useState([]); - const [filteredLabDevices] = useState(labDevices); const [firstDeviceOccupiedPorts, setFirstDeviceOccupiedPorts] = useState([]); - const [secondDeviceOccupiedPorts, setSecondDeviceOccupiedPorts] = useState([]); + const [secondDeviceOccupiedPorts, setSecondDeviceOccupiedPorts] = useState>({}); useEscapeKey(onClose); - // get a list of the current nodes in the topology to create a link - const nodes = getNodes(); - const currentNodesInTopology = new Set(nodes.map(n => n.data?.deviceData?.name)); - - // get occupied ports when selectedFirstDevice changes useEffect(() => { const edges = getEdges(); const ports = edges.filter((edge: Edge) => edge.source === selectedFirstDevice || edge.target === selectedFirstDevice).map((edge: Edge) => edge.id.split('-').filter((port: string) => port !== 'edge')); setFirstDeviceOccupiedPorts(ports.flat()); }, [selectedFirstDevice, getEdges]); - // get occupied ports when selectedSecondDevice changes - useEffect(() => { - const edges = getEdges(); - const ports = edges.filter((edge: Edge) => edge.source === selectedSecondDevice || edge.target === selectedSecondDevice).map((edge: Edge) => edge.id.split('-').filter((port: string) => port !== 'edge')); - setSecondDeviceOccupiedPorts(ports.flat()); - }, [selectedSecondDevice, getEdges]); - useEffect(() => { if (deviceData?.name) { setSelectedFirstDevice(deviceData.name); @@ -89,6 +76,31 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev }); } + useEffect(() => { + const edges = getEdges(); + if (selectedSecondDevice) { + const ports = edges + .filter((edge: Edge) => edge.source === selectedSecondDevice || edge.target === selectedSecondDevice) + .map((edge: Edge) => edge.id.split('-').filter((port: string) => port !== 'edge')); + setSecondDeviceOccupiedPorts((prev) => ({ + ...prev, + [selectedSecondDevice]: ports.flat(), + })); + } else { + // Initialize occupied ports for all devices on mount + const allOccupiedPorts = labDevices.reduce((acc, device) => { + const devicePorts = edges + .filter((edge: Edge) => edge.source === device.name || edge.target === device.name) + .map((edge: Edge) => edge.id.split('-').filter((port: string) => port !== 'edge')) + .flat(); + acc[device.name] = devicePorts; + return acc; + }, {} as Record); + + setSecondDeviceOccupiedPorts(allOccupiedPorts); + } + }, [selectedSecondDevice, getEdges, labDevices]); + return (
@@ -109,11 +121,16 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev disabled={!!deviceData?.name} > - {labDevices.filter((device) => device.userId == null || device.userId == user?.id).map((device) => ( - - ))} + {labDevices.filter((device) => device.userId == null || device.userId == user?.id).map((device) => { + const portsArray = device.ports.split(','); + const generatedPorts = portsArray.flatMap(portDef => generatePorts(portDef)); + const hasAvailablePorts = generatedPorts.some(port => !firstDeviceOccupiedPorts.includes(port)); + return ( + + ); + })}
@@ -122,9 +139,10 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev value={selectedFirstDevicePort} onChange={(e) => setSelectedFirstDevicePort(e.target.value)} className="block w-full mt-1 rounded-md bg-[#ffffff] focus:outline-none" + > - {(deviceData ? currentDevicePorts : filteredLabDevices.map(device => device.ports.split(',').flatMap(portDef => generatePorts(portDef)))).flat().map((port, index) => ( + {(deviceData ? currentDevicePorts : labDevices.map(device => device.ports.split(',').flatMap(portDef => generatePorts(portDef)))).flat().map((port, index) => ( @@ -139,11 +157,21 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev className="block w-full mt-1 rounded-md bg-[#ffffff] focus:outline-none" > - {filteredLabDevices.filter((device) => (device.userId == null || device.userId == user?.id) && currentNodesInTopology.has(device.name)).map((device) => ( - - ))} + {labDevices.filter((d) => d.name !== deviceData?.name).map((device) => { + const portsArray = device.ports.split(','); + const generatedPorts = portsArray.flatMap(portDef => generatePorts(portDef)); + const occupiedPorts = secondDeviceOccupiedPorts[device.name] || []; + const hasAvailablePorts = generatedPorts.some(port => !occupiedPorts.includes(port)); + return ( + + ); + })}
@@ -151,14 +179,18 @@ export default function CreateLinkModal({ deviceData, currentDevicePorts, labDev
diff --git a/frontend/src/hooks/useLinkOperations.ts b/frontend/src/hooks/useLinkOperations.ts index 58711b1..211450e 100644 --- a/frontend/src/hooks/useLinkOperations.ts +++ b/frontend/src/hooks/useLinkOperations.ts @@ -32,6 +32,7 @@ export function useLinkOperationsBase() { } const interconnectDevices = await authenticatedApiClient.getDevicesByType('INTERCONNECT'); + // only two devices here so find is okay return interconnectDevices.data?.find(d => d.name === connectionInfo.interconnectDeviceName); }; @@ -57,11 +58,15 @@ export function useLinkOperationsBase() { return true; }; - const removePortNumber = (port: string) => port.replace(/\/\d+$/, '/'); + const removePortNumber = (port?: string) => port?.split('|')[0]; const calculateOffsetPort = (port: string, deviceNumber: number) => { // splits off interface number and calculates offset - return Number(port.split('/').pop()) * deviceNumber; + let devicePort = Number(port.split('/').pop()) + if (deviceNumber === 2) { + devicePort += 44 // For IDs being fed to the VLAN mapping algorithm. + } + return devicePort; }; // API operations without ReactFlow dependencies @@ -99,29 +104,41 @@ export function useLinkOperationsBase() { } // Prepare link payload - const interconnect1Prefix = removePortNumber(firstConnectionInfo.interconnectDevicePort); - const interconnect2Prefix = removePortNumber(secondConnectionInfo.interconnectDevicePort); + // Get the correct interconnect information based on the device number for the interconnect + const [interconnect1, interconnect2] = firstInterconnectInfo?.deviceNumber === 1 + ? [firstInterconnectInfo, secondInterconnectInfo] + : [secondInterconnectInfo, firstInterconnectInfo]; + + const interconnect1Prefix = removePortNumber(interconnect1?.ports); + const interconnect2Prefix = removePortNumber(interconnect2?.ports); - const offsetPort1 = calculateOffsetPort( - firstConnectionInfo.interconnectDevicePort, - firstInterconnectInfo!.deviceNumber! - ); + // if ports is undefined + if (!interconnect1Prefix || !interconnect2Prefix) { + return false; + } - const offsetPort2 = calculateOffsetPort( - secondConnectionInfo.interconnectDevicePort, - secondInterconnectInfo!.deviceNumber! - ); + const [offsetPort1, offsetPort2] = firstInterconnectInfo?.deviceNumber === 1 + ? + [ + calculateOffsetPort(firstConnectionInfo.interconnectDevicePort, firstInterconnectInfo!.deviceNumber!), + calculateOffsetPort(secondConnectionInfo.interconnectDevicePort, secondInterconnectInfo!.deviceNumber!) + ] + : + [ + calculateOffsetPort(secondConnectionInfo.interconnectDevicePort, secondInterconnectInfo!.deviceNumber!), + calculateOffsetPort(firstConnectionInfo.interconnectDevicePort, firstInterconnectInfo!.deviceNumber!) + ] const linkPayload: LinkRequest = { - interconnect1IP: firstInterconnectInfo!.ipAddress, + interconnect1IP: interconnect1!.ipAddress, interconnect1Prefix, - interconnect2IP: secondInterconnectInfo!.ipAddress, + interconnect2IP: interconnect2!.ipAddress, interconnect2Prefix, interconnectPortID1: offsetPort1, interconnectPortID2: offsetPort2, - username: firstInterconnectInfo!.username!, - password: firstInterconnectInfo!.password!, - secret: firstInterconnectInfo!.secretPassword! + username: interconnect1!.username!, + password: interconnect1!.password!, + secret: interconnect1!.secretPassword! }; // Perform the requested operation @@ -301,6 +318,7 @@ export function useLinkOperations() { // Override the base methods to include ReactFlow operations const createLink = async (params: LinkOperationParams, createToastPerLink: boolean = true) => { + console.log(params); const edgeId = `edge-${params.firstDeviceName}-${params.firstDevicePort}-${params.secondDeviceName}-${params.secondDevicePort}`; createEdge(params); const result = await baseOperations.createLink(params, createToastPerLink); diff --git a/frontend/src/lib/helpers.ts b/frontend/src/lib/helpers.ts index 62df851..b0439c1 100644 --- a/frontend/src/lib/helpers.ts +++ b/frontend/src/lib/helpers.ts @@ -68,12 +68,18 @@ export const getCurvedPath = ( }; export function substringFromFirstNumber(port: string) { - const idx = port.search(/\d/); - if (idx !== -1) { - return port.substring(0, 3) + port.substring(idx); + // match up to the first 3 alphabetic characters followed by any numbers + const match = port.match(/^([a-zA-Z]{1,3})\D*(\d.*)$/); + + if (match) { + const prefix = match[1]; // up to the first 3 alphabetic characters + const numbers = match[2]; // all numbers after the prefix + return prefix + numbers; } + + // if no match, return an empty string return ""; -}; +} // validate email format export function validateEmail(email: string) { diff --git a/frontend/src/pages/Index.tsx b/frontend/src/pages/Index.tsx index 953fdb8..3b479ab 100644 --- a/frontend/src/pages/Index.tsx +++ b/frontend/src/pages/Index.tsx @@ -55,14 +55,14 @@ export default function IndexPage() {

- + - + diff --git a/package-lock.json b/package-lock.json index 9fbc8c8..65ac900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -833,7 +833,7 @@ "tailwindcss": "^3.4.14", "typescript": "^5.5.3", "typescript-eslint": "^8.7.0", - "vite": "^6.1.1", + "vite": "^6.2.4", "vite-plugin-svgr": "^4.3.0" } }, @@ -3562,9 +3562,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "cpu": [ "ppc64" ], @@ -3578,9 +3578,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "cpu": [ "arm" ], @@ -3594,9 +3594,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "cpu": [ "arm64" ], @@ -3610,9 +3610,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "cpu": [ "x64" ], @@ -3626,9 +3626,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "cpu": [ "arm64" ], @@ -3642,9 +3642,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "cpu": [ "x64" ], @@ -3658,9 +3658,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "cpu": [ "arm64" ], @@ -3674,9 +3674,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "cpu": [ "x64" ], @@ -3690,9 +3690,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "cpu": [ "arm" ], @@ -3706,9 +3706,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "cpu": [ "arm64" ], @@ -3722,9 +3722,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "cpu": [ "ia32" ], @@ -3738,9 +3738,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "cpu": [ "loong64" ], @@ -3754,9 +3754,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "cpu": [ "mips64el" ], @@ -3770,9 +3770,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "cpu": [ "ppc64" ], @@ -3786,9 +3786,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "cpu": [ "riscv64" ], @@ -3802,9 +3802,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "cpu": [ "s390x" ], @@ -3818,9 +3818,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "cpu": [ "x64" ], @@ -3834,9 +3834,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "cpu": [ "arm64" ], @@ -3850,9 +3850,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "cpu": [ "x64" ], @@ -3866,9 +3866,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "cpu": [ "arm64" ], @@ -3882,9 +3882,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "cpu": [ "x64" ], @@ -3898,9 +3898,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "cpu": [ "x64" ], @@ -3914,9 +3914,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "cpu": [ "arm64" ], @@ -3930,9 +3930,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "cpu": [ "ia32" ], @@ -3946,9 +3946,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "cpu": [ "x64" ], @@ -5301,9 +5301,9 @@ } }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "dev": true, "hasInstallScript": true, "bin": { @@ -5313,31 +5313,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" } }, "node_modules/escalade": { @@ -6694,13 +6694,13 @@ } }, "node_modules/vite": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz", - "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", + "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", "dev": true, "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.5.2", + "esbuild": "^0.25.0", + "postcss": "^8.5.3", "rollup": "^4.30.1" }, "bin": {