diff --git a/config.go b/config.go
index 435b87e..a09c426 100644
--- a/config.go
+++ b/config.go
@@ -22,8 +22,9 @@ type Config struct {
 	LocalAuthToken       string            `json:"local_auth_token"`
 	LocalAuthMode        string            `json:"localAuthMode"` //TODO: fix it with migration
 	WakeOnLanDevices     []WakeOnLanDevice `json:"wake_on_lan_devices"`
-	EdidString        string            `json:"hdmi_edid_string"`
-  DisplayMaxBrightness int               `json:"display_max_brightness"`
+	EdidString           string            `json:"hdmi_edid_string"`
+	ActiveExtension      string            `json:"active_extension"`
+	DisplayMaxBrightness int               `json:"display_max_brightness"`
 	DisplayDimAfterSec   int               `json:"display_dim_after_sec"`
 	DisplayOffAfterSec   int               `json:"display_off_after_sec"`
 }
@@ -33,6 +34,7 @@ const configPath = "/userdata/kvm_config.json"
 var defaultConfig = &Config{
 	CloudURL:             "https://api.jetkvm.com",
 	AutoUpdateEnabled:    true, // Set a default value
+	ActiveExtension:      "",
 	DisplayMaxBrightness: 64,
 	DisplayDimAfterSec:   120,  // 2 minutes
 	DisplayOffAfterSec:   1800, // 30 minutes
diff --git a/go.mod b/go.mod
index a1c9f87..adc054a 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/pojntfx/go-nbd v0.3.2
 	github.com/psanford/httpreadat v0.1.0
 	github.com/vishvananda/netlink v1.3.0
+	go.bug.st/serial v1.6.2
 	golang.org/x/crypto v0.28.0
 	golang.org/x/net v0.30.0
 )
@@ -33,6 +34,7 @@ require (
 	github.com/bytedance/sonic/loader v0.1.1 // indirect
 	github.com/cloudwego/base64x v0.1.4 // indirect
 	github.com/cloudwego/iasm v0.2.0 // indirect
+	github.com/creack/goselect v0.1.2 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-jose/go-jose/v4 v4.0.2 // indirect
@@ -69,7 +71,7 @@ require (
 	github.com/wlynxg/anet v0.0.5 // indirect
 	golang.org/x/arch v0.8.0 // indirect
 	golang.org/x/oauth2 v0.21.0 // indirect
-	golang.org/x/sys v0.26.0 // indirect
+	golang.org/x/sys v0.29.0 // indirect
 	golang.org/x/text v0.19.0 // indirect
 	google.golang.org/protobuf v1.34.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
diff --git a/go.sum b/go.sum
index 0b3c219..b7b8756 100644
--- a/go.sum
+++ b/go.sum
@@ -16,6 +16,8 @@ github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NA
 github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
 github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
 github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
+github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
+github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
 github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@@ -146,6 +148,8 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
 github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
 github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
+go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
+go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
 golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
@@ -161,8 +165,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
 golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
diff --git a/jsonrpc.go b/jsonrpc.go
index 45ed56e..aa39d25 100644
--- a/jsonrpc.go
+++ b/jsonrpc.go
@@ -10,8 +10,11 @@ import (
 	"os/exec"
 	"path/filepath"
 	"reflect"
+	"strconv"
+	"time"
 
 	"github.com/pion/webrtc/v4"
+	"go.bug.st/serial"
 )
 
 type JSONRPCRequest struct {
@@ -569,7 +572,172 @@ func rpcResetConfig() error {
 	return nil
 }
 
-// TODO: replace this crap with code generator
+type DCPowerState struct {
+	IsOn    bool    `json:"isOn"`
+	Voltage float64 `json:"voltage"`
+	Current float64 `json:"current"`
+	Power   float64 `json:"power"`
+}
+
+func rpcGetDCPowerState() (DCPowerState, error) {
+	return dcState, nil
+}
+
+func rpcSetDCPowerState(enabled bool) error {
+	log.Printf("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled)
+	err := setDCPowerState(enabled)
+	if err != nil {
+		return fmt.Errorf("failed to set DC power state: %w", err)
+	}
+	return nil
+}
+
+func rpcGetActiveExtension() (string, error) {
+	return config.ActiveExtension, nil
+}
+
+func rpcSetActiveExtension(extensionId string) error {
+	if config.ActiveExtension == extensionId {
+		return nil
+	}
+	if config.ActiveExtension == "atx-power" {
+		unmountATXControl()
+	} else if config.ActiveExtension == "dc-power" {
+		unmountDCControl()
+	}
+	config.ActiveExtension = extensionId
+	if err := SaveConfig(); err != nil {
+		return fmt.Errorf("failed to save config: %w", err)
+	}
+	if extensionId == "atx-power" {
+		mountATXControl()
+	} else if extensionId == "dc-power" {
+		mountDCControl()
+	}
+	return nil
+}
+
+func rpcSetATXPowerAction(action string) error {
+	logger.Debugf("[jsonrpc.go:rpcSetATXPowerAction] Executing ATX power action: %s", action)
+	switch action {
+	case "power-short":
+		logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating short power button press")
+		return pressATXPowerButton(200 * time.Millisecond)
+	case "power-long":
+		logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating long power button press")
+		return pressATXPowerButton(5 * time.Second)
+	case "reset":
+		logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating reset button press")
+		return pressATXResetButton(200 * time.Millisecond)
+	default:
+		return fmt.Errorf("invalid action: %s", action)
+	}
+}
+
+type ATXState struct {
+	Power bool `json:"power"`
+	HDD   bool `json:"hdd"`
+}
+
+func rpcGetATXState() (ATXState, error) {
+	state := ATXState{
+		Power: ledPWRState,
+		HDD:   ledHDDState,
+	}
+	return state, nil
+}
+
+type SerialSettings struct {
+	BaudRate string `json:"baudRate"`
+	DataBits string `json:"dataBits"`
+	StopBits string `json:"stopBits"`
+	Parity   string `json:"parity"`
+}
+
+func rpcGetSerialSettings() (SerialSettings, error) {
+	settings := SerialSettings{
+		BaudRate: strconv.Itoa(serialPortMode.BaudRate),
+		DataBits: strconv.Itoa(serialPortMode.DataBits),
+		StopBits: "1",
+		Parity:   "none",
+	}
+
+	switch serialPortMode.StopBits {
+	case serial.OneStopBit:
+		settings.StopBits = "1"
+	case serial.OnePointFiveStopBits:
+		settings.StopBits = "1.5"
+	case serial.TwoStopBits:
+		settings.StopBits = "2"
+	}
+
+	switch serialPortMode.Parity {
+	case serial.NoParity:
+		settings.Parity = "none"
+	case serial.OddParity:
+		settings.Parity = "odd"
+	case serial.EvenParity:
+		settings.Parity = "even"
+	case serial.MarkParity:
+		settings.Parity = "mark"
+	case serial.SpaceParity:
+		settings.Parity = "space"
+	}
+
+	return settings, nil
+}
+
+var serialPortMode = defaultMode
+
+func rpcSetSerialSettings(settings SerialSettings) error {
+	baudRate, err := strconv.Atoi(settings.BaudRate)
+	if err != nil {
+		return fmt.Errorf("invalid baud rate: %v", err)
+	}
+	dataBits, err := strconv.Atoi(settings.DataBits)
+	if err != nil {
+		return fmt.Errorf("invalid data bits: %v", err)
+	}
+
+	var stopBits serial.StopBits
+	switch settings.StopBits {
+	case "1":
+		stopBits = serial.OneStopBit
+	case "1.5":
+		stopBits = serial.OnePointFiveStopBits
+	case "2":
+		stopBits = serial.TwoStopBits
+	default:
+		return fmt.Errorf("invalid stop bits: %s", settings.StopBits)
+	}
+
+	var parity serial.Parity
+	switch settings.Parity {
+	case "none":
+		parity = serial.NoParity
+	case "odd":
+		parity = serial.OddParity
+	case "even":
+		parity = serial.EvenParity
+	case "mark":
+		parity = serial.MarkParity
+	case "space":
+		parity = serial.SpaceParity
+	default:
+		return fmt.Errorf("invalid parity: %s", settings.Parity)
+	}
+	serialPortMode = &serial.Mode{
+		BaudRate: baudRate,
+		DataBits: dataBits,
+		StopBits: stopBits,
+		Parity:   parity,
+	}
+
+	port.SetMode(serialPortMode)
+
+	return nil
+}
+
 var rpcHandlers = map[string]RPCHandler{
 	"ping":                   {Func: rpcPing},
 	"getDeviceID":            {Func: rpcGetDeviceID},
@@ -618,4 +786,12 @@ var rpcHandlers = map[string]RPCHandler{
 	"resetConfig":            {Func: rpcResetConfig},
 	"setBacklightSettings":   {Func: rpcSetBacklightSettings, Params: []string{"params"}},
 	"getBacklightSettings":   {Func: rpcGetBacklightSettings},
+	"getDCPowerState":        {Func: rpcGetDCPowerState},
+	"setDCPowerState":        {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
+	"getActiveExtension":     {Func: rpcGetActiveExtension},
+	"setActiveExtension":     {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
+	"getATXState":            {Func: rpcGetATXState},
+	"setATXPowerAction":      {Func: rpcSetATXPowerAction, Params: []string{"action"}},
+	"getSerialSettings":      {Func: rpcGetSerialSettings},
+	"setSerialSettings":      {Func: rpcSetSerialSettings, Params: []string{"settings"}},
 }
diff --git a/main.go b/main.go
index 7ff771f..e23e9c8 100644
--- a/main.go
+++ b/main.go
@@ -71,6 +71,7 @@ func Main() {
 	if config.CloudToken != "" {
 		go RunWebsocketClient()
 	}
+	initSerialPort()
 	sigs := make(chan os.Signal, 1)
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 	<-sigs
diff --git a/serial.go b/serial.go
new file mode 100644
index 0000000..3ad56d3
--- /dev/null
+++ b/serial.go
@@ -0,0 +1,269 @@
+package kvm
+
+import (
+	"bufio"
+	"io"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/pion/webrtc/v4"
+	"go.bug.st/serial"
+)
+
+const serialPortPath = "/dev/ttyS3"
+
+var port serial.Port
+
+func mountATXControl() error {
+	port.SetMode(defaultMode)
+	go runATXControl()
+
+	return nil
+}
+
+func unmountATXControl() error {
+	reopenSerialPort()
+	return nil
+}
+
+var (
+	ledHDDState bool
+	ledPWRState bool
+	btnRSTState bool
+	btnPWRState bool
+)
+
+func runATXControl() {
+	reader := bufio.NewReader(port)
+	for {
+		line, err := reader.ReadString('\n')
+		if err != nil {
+			logger.Errorf("Error reading from serial port: %v", err)
+			return
+		}
+
+		// Each line should be 4 binary digits + newline
+		if len(line) != 5 {
+			logger.Warnf("Invalid line length: %d", len(line))
+			continue
+		}
+
+		// Parse new states
+		newLedHDDState := line[0] == '0'
+		newLedPWRState := line[1] == '0'
+		newBtnRSTState := line[2] == '1'
+		newBtnPWRState := line[3] == '1'
+
+		if currentSession != nil {
+			writeJSONRPCEvent("atxState", ATXState{
+				Power: newLedPWRState,
+				HDD:   newLedHDDState,
+			}, currentSession)
+		}
+
+		if newLedHDDState != ledHDDState ||
+			newLedPWRState != ledPWRState ||
+			newBtnRSTState != btnRSTState ||
+			newBtnPWRState != btnPWRState {
+
+			logger.Debugf("Status changed: HDD LED: %v, PWR LED: %v, RST BTN: %v, PWR BTN: %v",
+				newLedHDDState, newLedPWRState, newBtnRSTState, newBtnPWRState)
+
+			// Update states
+			ledHDDState = newLedHDDState
+			ledPWRState = newLedPWRState
+			btnRSTState = newBtnRSTState
+			btnPWRState = newBtnPWRState
+		}
+	}
+}
+
+func pressATXPowerButton(duration time.Duration) error {
+	_, err := port.Write([]byte("\n"))
+	if err != nil {
+		return err
+	}
+
+	_, err = port.Write([]byte("BTN_PWR_ON\n"))
+	if err != nil {
+		return err
+	}
+
+	time.Sleep(duration)
+
+	_, err = port.Write([]byte("BTN_PWR_OFF\n"))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func pressATXResetButton(duration time.Duration) error {
+	_, err := port.Write([]byte("\n"))
+	if err != nil {
+		return err
+	}
+
+	_, err = port.Write([]byte("BTN_RST_ON\n"))
+	if err != nil {
+		return err
+	}
+
+	time.Sleep(duration)
+
+	_, err = port.Write([]byte("BTN_RST_OFF\n"))
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func mountDCControl() error {
+	port.SetMode(defaultMode)
+	go runDCControl()
+	return nil
+}
+
+func unmountDCControl() error {
+	reopenSerialPort()
+	return nil
+}
+
+var dcState DCPowerState
+
+func runDCControl() {
+	reader := bufio.NewReader(port)
+	for {
+		line, err := reader.ReadString('\n')
+		if err != nil {
+			logger.Errorf("Error reading from serial port: %v", err)
+			return
+		}
+
+		// Split the line by semicolon
+		parts := strings.Split(strings.TrimSpace(line), ";")
+		if len(parts) != 4 {
+			logger.Warnf("Invalid line: %s", line)
+			continue
+		}
+
+		// Parse new states
+		powerState, err := strconv.Atoi(parts[0])
+		if err != nil {
+			logger.Warnf("Invalid power state: %v", err)
+			continue
+		}
+		dcState.IsOn = powerState == 1
+		milliVolts, err := strconv.ParseFloat(parts[1], 64)
+		if err != nil {
+			logger.Warnf("Invalid voltage: %v", err)
+			continue
+		}
+		volts := milliVolts / 1000 // Convert mV to V
+
+		milliAmps, err := strconv.ParseFloat(parts[2], 64)
+		if err != nil {
+			logger.Warnf("Invalid current: %v", err)
+			continue
+		}
+		amps := milliAmps / 1000 // Convert mA to A
+
+		milliWatts, err := strconv.ParseFloat(parts[3], 64)
+		if err != nil {
+			logger.Warnf("Invalid power: %v", err)
+			continue
+		}
+		watts := milliWatts / 1000 // Convert mW to W
+
+		dcState.Voltage = volts
+		dcState.Current = amps
+		dcState.Power = watts
+
+		if currentSession != nil {
+			writeJSONRPCEvent("dcState", dcState, currentSession)
+		}
+	}
+}
+
+func setDCPowerState(on bool) error {
+	_, err := port.Write([]byte("\n"))
+	if err != nil {
+		return err
+	}
+	command := "PWR_OFF\n"
+	if on {
+		command = "PWR_ON\n"
+	}
+	_, err = port.Write([]byte(command))
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+var defaultMode = &serial.Mode{
+	BaudRate: 115200,
+	DataBits: 8,
+	Parity:   serial.NoParity,
+	StopBits: serial.OneStopBit,
+}
+
+func initSerialPort() {
+	reopenSerialPort()
+	if config.ActiveExtension == "atx-power" {
+		mountATXControl()
+	} else if config.ActiveExtension == "dc-power" {
+		mountDCControl()
+	}
+}
+
+func reopenSerialPort() error {
+	if port != nil {
+		port.Close()
+	}
+	var err error
+	port, err = serial.Open(serialPortPath, defaultMode)
+	if err != nil {
+		logger.Errorf("Error opening serial port: %v", err)
+	}
+	return nil
+}
+
+func handleSerialChannel(d *webrtc.DataChannel) {
+	d.OnOpen(func() {
+		go func() {
+			buf := make([]byte, 1024)
+			for {
+				n, err := port.Read(buf)
+				if err != nil {
+					if err != io.EOF {
+						logger.Errorf("Failed to read from serial port: %v", err)
+					}
+					break
+				}
+				err = d.Send(buf[:n])
+				if err != nil {
+					logger.Errorf("Failed to send serial output: %v", err)
+					break
+				}
+			}
+		}()
+	})
+
+	d.OnMessage(func(msg webrtc.DataChannelMessage) {
+		if port == nil {
+			return
+		}
+		_, err := port.Write(msg.Data)
+		if err != nil {
+			logger.Errorf("Failed to write to serial: %v", err)
+		}
+	})
+
+	d.OnClose(func() {
+
+	})
+}
diff --git a/ui/.env.staging b/ui/.env.staging
new file mode 100644
index 0000000..651e5bc
--- /dev/null
+++ b/ui/.env.staging
@@ -0,0 +1,4 @@
+VITE_SIGNAL_API=https://staging-api.jetkvm.com
+
+VITE_CLOUD_APP=https://staging-app.jetkvm.com
+VITE_CLOUD_API=https://staging-api.jetkvm.com
\ No newline at end of file
diff --git a/ui/package-lock.json b/ui/package-lock.json
index e60ce6f..0ba5323 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -8,17 +8,18 @@
       "name": "kvm-ui",
       "version": "0.0.0",
       "dependencies": {
-        "@headlessui/react": "^2.1.10",
-        "@headlessui/tailwindcss": "^0.2.0",
-        "@heroicons/react": "^2.1.3",
+        "@headlessui/react": "^2.2.0",
+        "@headlessui/tailwindcss": "^0.2.1",
+        "@heroicons/react": "^2.2.0",
         "@xterm/addon-clipboard": "^0.1.0",
         "@xterm/addon-fit": "^0.10.0",
         "@xterm/addon-unicode11": "^0.8.0",
         "@xterm/addon-web-links": "^0.11.0",
         "@xterm/addon-webgl": "^0.18.0",
+        "@xterm/xterm": "^5.5.0",
         "cva": "^1.0.0-beta.1",
         "focus-trap-react": "^10.2.3",
-        "framer-motion": "^11.0.28",
+        "framer-motion": "^11.15.0",
         "lodash.throttle": "^4.1.1",
         "mini-svg-data-uri": "^1.4.4",
         "react": "^18.2.0",
@@ -28,32 +29,33 @@
         "react-icons": "^5.4.0",
         "react-router-dom": "^6.22.3",
         "react-simple-keyboard": "^3.7.112",
-        "recharts": "^2.12.6",
-        "tailwind-merge": "^2.2.2",
+        "react-xtermjs": "^1.0.9",
+        "recharts": "^2.15.0",
+        "tailwind-merge": "^2.5.5",
         "usehooks-ts": "^3.1.0",
         "validator": "^13.12.0",
         "xterm": "^5.3.0",
         "zustand": "^4.5.2"
       },
       "devDependencies": {
-        "@tailwindcss/forms": "^0.5.7",
-        "@tailwindcss/typography": "^0.5.12",
+        "@tailwindcss/forms": "^0.5.9",
+        "@tailwindcss/typography": "^0.5.15",
         "@types/react": "^18.2.66",
         "@types/react-dom": "^18.3.0",
         "@types/validator": "^13.12.2",
         "@typescript-eslint/eslint-plugin": "^7.2.0",
         "@typescript-eslint/parser": "^7.2.0",
-        "@vitejs/plugin-react-swc": "^3.5.0",
-        "autoprefixer": "^10.4.19",
+        "@vitejs/plugin-react-swc": "^3.7.2",
+        "autoprefixer": "^10.4.20",
         "eslint": "^8.57.0",
         "eslint-plugin-react": "^7.34.1",
         "eslint-plugin-react-hooks": "^4.6.0",
         "eslint-plugin-react-refresh": "^0.4.6",
-        "postcss": "^8.4.38",
-        "prettier": "^3.2.5",
+        "postcss": "^8.4.49",
+        "prettier": "^3.4.2",
         "prettier-plugin-tailwindcss": "^0.5.13",
-        "tailwindcss": "^3.4.3",
-        "typescript": "^5.2.2",
+        "tailwindcss": "^3.4.17",
+        "typescript": "^5.7.2",
         "vite": "^5.2.0",
         "vite-tsconfig-paths": "^4.3.2"
       },
@@ -587,9 +589,9 @@
       "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA=="
     },
     "node_modules/@headlessui/react": {
-      "version": "2.1.10",
-      "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.10.tgz",
-      "integrity": "sha512-6mLa2fjMDAFQi+/R10B+zU3edsUk/MDtENB2zHho0lqKU1uzhAfJLUduWds4nCo8wbl3vULtC5rJfZAQ1yqIng==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.0.tgz",
+      "integrity": "sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==",
       "dependencies": {
         "@floating-ui/react": "^0.26.16",
         "@react-aria/focus": "^3.17.1",
@@ -600,14 +602,14 @@
         "node": ">=10"
       },
       "peerDependencies": {
-        "react": "^18",
-        "react-dom": "^18"
+        "react": "^18 || ^19 || ^19.0.0-rc",
+        "react-dom": "^18 || ^19 || ^19.0.0-rc"
       }
     },
     "node_modules/@headlessui/tailwindcss": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz",
-      "integrity": "sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==",
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.1.tgz",
+      "integrity": "sha512-2+5+NZ+RzMyrVeCZOxdbvkUSssSxGvcUxphkIfSVLpRiKsj+/63T2TOL9dBYMXVfj/CGr6hMxSRInzXv6YY7sA==",
       "engines": {
         "node": ">=10"
       },
@@ -616,11 +618,11 @@
       }
     },
     "node_modules/@heroicons/react": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.3.tgz",
-      "integrity": "sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
+      "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
       "peerDependencies": {
-        "react": ">= 16"
+        "react": ">= 16 || ^19.0.0-rc"
       }
     },
     "node_modules/@humanwhocodes/config-array": {
@@ -1084,14 +1086,14 @@
       ]
     },
     "node_modules/@swc/core": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.12.tgz",
-      "integrity": "sha512-QljRxTaUajSLB9ui93cZ38/lmThwIw/BPxjn+TphrYN6LPU3vu9/ykjgHtlpmaXDDcngL4K5i396E7iwwEUxYg==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz",
+      "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==",
       "dev": true,
       "hasInstallScript": true,
       "dependencies": {
-        "@swc/counter": "^0.1.2",
-        "@swc/types": "^0.1.5"
+        "@swc/counter": "^0.1.3",
+        "@swc/types": "^0.1.17"
       },
       "engines": {
         "node": ">=10"
@@ -1101,19 +1103,19 @@
         "url": "https://opencollective.com/swc"
       },
       "optionalDependencies": {
-        "@swc/core-darwin-arm64": "1.4.12",
-        "@swc/core-darwin-x64": "1.4.12",
-        "@swc/core-linux-arm-gnueabihf": "1.4.12",
-        "@swc/core-linux-arm64-gnu": "1.4.12",
-        "@swc/core-linux-arm64-musl": "1.4.12",
-        "@swc/core-linux-x64-gnu": "1.4.12",
-        "@swc/core-linux-x64-musl": "1.4.12",
-        "@swc/core-win32-arm64-msvc": "1.4.12",
-        "@swc/core-win32-ia32-msvc": "1.4.12",
-        "@swc/core-win32-x64-msvc": "1.4.12"
+        "@swc/core-darwin-arm64": "1.10.1",
+        "@swc/core-darwin-x64": "1.10.1",
+        "@swc/core-linux-arm-gnueabihf": "1.10.1",
+        "@swc/core-linux-arm64-gnu": "1.10.1",
+        "@swc/core-linux-arm64-musl": "1.10.1",
+        "@swc/core-linux-x64-gnu": "1.10.1",
+        "@swc/core-linux-x64-musl": "1.10.1",
+        "@swc/core-win32-arm64-msvc": "1.10.1",
+        "@swc/core-win32-ia32-msvc": "1.10.1",
+        "@swc/core-win32-x64-msvc": "1.10.1"
       },
       "peerDependencies": {
-        "@swc/helpers": "^0.5.0"
+        "@swc/helpers": "*"
       },
       "peerDependenciesMeta": {
         "@swc/helpers": {
@@ -1122,9 +1124,9 @@
       }
     },
     "node_modules/@swc/core-darwin-arm64": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.12.tgz",
-      "integrity": "sha512-BZUUq91LGJsLI2BQrhYL3yARkcdN4TS3YGNS6aRYUtyeWrGCTKHL90erF2BMU2rEwZLLkOC/U899R4o4oiSHfA==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz",
+      "integrity": "sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==",
       "cpu": [
         "arm64"
       ],
@@ -1138,9 +1140,9 @@
       }
     },
     "node_modules/@swc/core-darwin-x64": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.12.tgz",
-      "integrity": "sha512-Wkk8rq1RwCOgg5ybTlfVtOYXLZATZ+QjgiBNM7pIn03A5/zZicokNTYd8L26/mifly2e74Dz34tlIZBT4aTGDA==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz",
+      "integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==",
       "cpu": [
         "x64"
       ],
@@ -1154,9 +1156,9 @@
       }
     },
     "node_modules/@swc/core-linux-arm-gnueabihf": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.12.tgz",
-      "integrity": "sha512-8jb/SN67oTQ5KSThWlKLchhU6xnlAlnmnLCCOKK1xGtFS6vD+By9uL+qeEY2krV98UCRTf68WSmC0SLZhVoz5A==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz",
+      "integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==",
       "cpu": [
         "arm"
       ],
@@ -1170,9 +1172,9 @@
       }
     },
     "node_modules/@swc/core-linux-arm64-gnu": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.12.tgz",
-      "integrity": "sha512-DhW47DQEZKCdSq92v5F03rqdpjRXdDMqxfu4uAlZ9Uo1wJEGvY23e1SNmhji2sVHsZbBjSvoXoBLk0v00nSG8w==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz",
+      "integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==",
       "cpu": [
         "arm64"
       ],
@@ -1186,9 +1188,9 @@
       }
     },
     "node_modules/@swc/core-linux-arm64-musl": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.12.tgz",
-      "integrity": "sha512-PR57pT3TssnCRvdsaKNsxZy9N8rFg9AKA1U7W+LxbZ/7Z7PHc5PjxF0GgZpE/aLmU6xOn5VyQTlzjoamVkt05g==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz",
+      "integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==",
       "cpu": [
         "arm64"
       ],
@@ -1202,9 +1204,9 @@
       }
     },
     "node_modules/@swc/core-linux-x64-gnu": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.12.tgz",
-      "integrity": "sha512-HLZIWNHWuFIlH+LEmXr1lBiwGQeCshKOGcqbJyz7xpqTh7m2IPAxPWEhr/qmMTMsjluGxeIsLrcsgreTyXtgNA==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.1.tgz",
+      "integrity": "sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==",
       "cpu": [
         "x64"
       ],
@@ -1218,9 +1220,9 @@
       }
     },
     "node_modules/@swc/core-linux-x64-musl": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.12.tgz",
-      "integrity": "sha512-M5fBAtoOcpz2YQAFtNemrPod5BqmzAJc8pYtT3dVTn1MJllhmLHlphU8BQytvoGr1PHgJL8ZJBlBGdt70LQ7Mw==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz",
+      "integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==",
       "cpu": [
         "x64"
       ],
@@ -1234,9 +1236,9 @@
       }
     },
     "node_modules/@swc/core-win32-arm64-msvc": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.12.tgz",
-      "integrity": "sha512-K8LjjgZ7VQFtM+eXqjfAJ0z+TKVDng3r59QYn7CL6cyxZI2brLU3lNknZcUFSouZD+gsghZI/Zb8tQjVk7aKDQ==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz",
+      "integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==",
       "cpu": [
         "arm64"
       ],
@@ -1250,9 +1252,9 @@
       }
     },
     "node_modules/@swc/core-win32-ia32-msvc": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.12.tgz",
-      "integrity": "sha512-hflO5LCxozngoOmiQbDPyvt6ODc5Cu9AwTJP9uH/BSMPdEQ6PCnefuUOJLAKew2q9o+NmDORuJk+vgqQz9Uzpg==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz",
+      "integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==",
       "cpu": [
         "ia32"
       ],
@@ -1266,9 +1268,9 @@
       }
     },
     "node_modules/@swc/core-win32-x64-msvc": {
-      "version": "1.4.12",
-      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.12.tgz",
-      "integrity": "sha512-3A4qMtddBDbtprV5edTB/SgJn9L+X5TL7RGgS3eWtEgn/NG8gA80X/scjf1v2MMeOsrcxiYhnemI2gXCKuQN2g==",
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz",
+      "integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==",
       "cpu": [
         "x64"
       ],
@@ -1296,30 +1298,30 @@
       }
     },
     "node_modules/@swc/types": {
-      "version": "0.1.6",
-      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz",
-      "integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==",
+      "version": "0.1.17",
+      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
+      "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
       "dev": true,
       "dependencies": {
         "@swc/counter": "^0.1.3"
       }
     },
     "node_modules/@tailwindcss/forms": {
-      "version": "0.5.7",
-      "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz",
-      "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==",
+      "version": "0.5.9",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.9.tgz",
+      "integrity": "sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==",
       "dev": true,
       "dependencies": {
         "mini-svg-data-uri": "^1.2.3"
       },
       "peerDependencies": {
-        "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1"
+        "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20"
       }
     },
     "node_modules/@tailwindcss/typography": {
-      "version": "0.5.12",
-      "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.12.tgz",
-      "integrity": "sha512-CNwpBpconcP7ppxmuq3qvaCxiRWnbhANpY/ruH4L5qs2GCiVDJXde/pjj2HWPV1+Q4G9+V/etrwUYopdcjAlyg==",
+      "version": "0.5.15",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.15.tgz",
+      "integrity": "sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==",
       "dev": true,
       "dependencies": {
         "lodash.castarray": "^4.4.0",
@@ -1328,7 +1330,7 @@
         "postcss-selector-parser": "6.0.10"
       },
       "peerDependencies": {
-        "tailwindcss": ">=3.0.0 || insiders"
+        "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20"
       }
     },
     "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
@@ -1669,15 +1671,15 @@
       "dev": true
     },
     "node_modules/@vitejs/plugin-react-swc": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.6.0.tgz",
-      "integrity": "sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==",
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.2.tgz",
+      "integrity": "sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==",
       "dev": true,
       "dependencies": {
-        "@swc/core": "^1.3.107"
+        "@swc/core": "^1.7.26"
       },
       "peerDependencies": {
-        "vite": "^4 || ^5"
+        "vite": "^4 || ^5 || ^6"
       }
     },
     "node_modules/@xterm/addon-clipboard": {
@@ -1726,8 +1728,7 @@
     "node_modules/@xterm/xterm": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
-      "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
-      "peer": true
+      "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="
     },
     "node_modules/acorn": {
       "version": "8.11.3",
@@ -1965,9 +1966,9 @@
       }
     },
     "node_modules/autoprefixer": {
-      "version": "10.4.19",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
-      "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
+      "version": "10.4.20",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+      "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
       "dev": true,
       "funding": [
         {
@@ -1984,11 +1985,11 @@
         }
       ],
       "dependencies": {
-        "browserslist": "^4.23.0",
-        "caniuse-lite": "^1.0.30001599",
+        "browserslist": "^4.23.3",
+        "caniuse-lite": "^1.0.30001646",
         "fraction.js": "^4.3.7",
         "normalize-range": "^0.1.2",
-        "picocolors": "^1.0.0",
+        "picocolors": "^1.0.1",
         "postcss-value-parser": "^4.2.0"
       },
       "bin": {
@@ -2041,20 +2042,20 @@
       }
     },
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/browserslist": {
-      "version": "4.23.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
-      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
+      "version": "4.24.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
+      "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
       "dev": true,
       "funding": [
         {
@@ -2071,10 +2072,10 @@
         }
       ],
       "dependencies": {
-        "caniuse-lite": "^1.0.30001587",
-        "electron-to-chromium": "^1.4.668",
-        "node-releases": "^2.0.14",
-        "update-browserslist-db": "^1.0.13"
+        "caniuse-lite": "^1.0.30001688",
+        "electron-to-chromium": "^1.5.73",
+        "node-releases": "^2.0.19",
+        "update-browserslist-db": "^1.1.1"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -2120,9 +2121,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001666",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz",
-      "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==",
+      "version": "1.0.30001690",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
+      "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
       "dev": true,
       "funding": [
         {
@@ -2547,9 +2548,9 @@
       "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.729",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz",
-      "integrity": "sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==",
+      "version": "1.5.75",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
+      "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
       "dev": true
     },
     "node_modules/emoji-regex": {
@@ -2754,9 +2755,9 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
-      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
       "dev": true,
       "engines": {
         "node": ">=6"
@@ -3112,9 +3113,9 @@
       }
     },
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dependencies": {
         "to-regex-range": "^5.0.1"
       },
@@ -3218,16 +3219,18 @@
       }
     },
     "node_modules/framer-motion": {
-      "version": "11.0.28",
-      "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.28.tgz",
-      "integrity": "sha512-j/vNYTCH5MX5sY/3dwMs00z1+qAqKX3iIHF762bwqlU814ooD5dDbuj3pA0LmIT5YqyryCkXEb/q+zRblin0lw==",
+      "version": "11.15.0",
+      "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.15.0.tgz",
+      "integrity": "sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==",
       "dependencies": {
+        "motion-dom": "^11.14.3",
+        "motion-utils": "^11.14.3",
         "tslib": "^2.4.0"
       },
       "peerDependencies": {
         "@emotion/is-prop-valid": "*",
-        "react": "^18.0.0",
-        "react-dom": "^18.0.0"
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
       },
       "peerDependenciesMeta": {
         "@emotion/is-prop-valid": {
@@ -4021,9 +4024,9 @@
       }
     },
     "node_modules/jiti": {
-      "version": "1.21.0",
-      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
-      "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
+      "version": "1.21.7",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
       "bin": {
         "jiti": "bin/jiti.js"
       }
@@ -4106,11 +4109,14 @@
       }
     },
     "node_modules/lilconfig": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
-      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+      "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
       "engines": {
-        "node": ">=10"
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
       }
     },
     "node_modules/lines-and-columns": {
@@ -4198,11 +4204,11 @@
       }
     },
     "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dependencies": {
-        "braces": "^3.0.2",
+        "braces": "^3.0.3",
         "picomatch": "^2.3.1"
       },
       "engines": {
@@ -4239,6 +4245,16 @@
         "node": ">=16 || 14 >=14.17"
       }
     },
+    "node_modules/motion-dom": {
+      "version": "11.14.3",
+      "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.14.3.tgz",
+      "integrity": "sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA=="
+    },
+    "node_modules/motion-utils": {
+      "version": "11.14.3",
+      "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.14.3.tgz",
+      "integrity": "sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ=="
+    },
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -4279,9 +4295,9 @@
       "dev": true
     },
     "node_modules/node-releases": {
-      "version": "2.0.14",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
-      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
+      "version": "2.0.19",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+      "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
       "dev": true
     },
     "node_modules/normalize-path": {
@@ -4551,9 +4567,9 @@
       }
     },
     "node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
     },
     "node_modules/picomatch": {
       "version": "2.3.1",
@@ -4592,9 +4608,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.38",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
-      "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+      "version": "8.4.49",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+      "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
       "funding": [
         {
           "type": "opencollective",
@@ -4611,8 +4627,8 @@
       ],
       "dependencies": {
         "nanoid": "^3.3.7",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.2.0"
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
@@ -4702,39 +4718,34 @@
         }
       }
     },
-    "node_modules/postcss-load-config/node_modules/lilconfig": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
-      "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
-      "engines": {
-        "node": ">=14"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antonk52"
-      }
-    },
     "node_modules/postcss-nested": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
-      "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
       "dependencies": {
-        "postcss-selector-parser": "^6.0.11"
+        "postcss-selector-parser": "^6.1.1"
       },
       "engines": {
         "node": ">=12.0"
       },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/postcss/"
-      },
       "peerDependencies": {
         "postcss": "^8.2.14"
       }
     },
     "node_modules/postcss-selector-parser": {
-      "version": "6.0.16",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
-      "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
       "dependencies": {
         "cssesc": "^3.0.0",
         "util-deprecate": "^1.0.2"
@@ -4758,9 +4769,9 @@
       }
     },
     "node_modules/prettier": {
-      "version": "3.2.5",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
-      "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
+      "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
       "dev": true,
       "bin": {
         "prettier": "bin/prettier.cjs"
@@ -5015,6 +5026,14 @@
         "react-dom": ">=16.6.0"
       }
     },
+    "node_modules/react-xtermjs": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.9.tgz",
+      "integrity": "sha512-lrK1xiWfgxAC+4shtMHh0Irxg2t5t7JbTtpP0W7GIf1gQ9SHW/djmyiLpQSA75mN1DpT0bKeqj1fOKd0XX8RBA==",
+      "peerDependencies": {
+        "@xterm/xterm": "^5.5.0"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -5035,14 +5054,14 @@
       }
     },
     "node_modules/recharts": {
-      "version": "2.12.6",
-      "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.6.tgz",
-      "integrity": "sha512-D+7j9WI+D0NHauah3fKHuNNcRK8bOypPW7os1DERinogGBGaHI7i6tQKJ0aUF3JXyBZ63dyfKIW2WTOPJDxJ8w==",
+      "version": "2.15.0",
+      "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz",
+      "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==",
       "dependencies": {
         "clsx": "^2.0.0",
         "eventemitter3": "^4.0.1",
         "lodash": "^4.17.21",
-        "react-is": "^16.10.2",
+        "react-is": "^18.3.1",
         "react-smooth": "^4.0.0",
         "recharts-scale": "^0.4.4",
         "tiny-invariant": "^1.3.1",
@@ -5052,8 +5071,8 @@
         "node": ">=14"
       },
       "peerDependencies": {
-        "react": "^16.0.0 || ^17.0.0 || ^18.0.0",
-        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
+        "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
       }
     },
     "node_modules/recharts-scale": {
@@ -5064,6 +5083,11 @@
         "decimal.js-light": "^2.4.1"
       }
     },
+    "node_modules/recharts/node_modules/react-is": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+      "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
+    },
     "node_modules/reflect.getprototypeof": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@@ -5362,9 +5386,9 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -5610,44 +5634,41 @@
       "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
     },
     "node_modules/tailwind-merge": {
-      "version": "2.2.2",
-      "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.2.tgz",
-      "integrity": "sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==",
-      "dependencies": {
-        "@babel/runtime": "^7.24.0"
-      },
+      "version": "2.5.5",
+      "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
+      "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/dcastil"
       }
     },
     "node_modules/tailwindcss": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
-      "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
+      "version": "3.4.17",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+      "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
       "dependencies": {
         "@alloc/quick-lru": "^5.2.0",
         "arg": "^5.0.2",
-        "chokidar": "^3.5.3",
+        "chokidar": "^3.6.0",
         "didyoumean": "^1.2.2",
         "dlv": "^1.1.3",
-        "fast-glob": "^3.3.0",
+        "fast-glob": "^3.3.2",
         "glob-parent": "^6.0.2",
         "is-glob": "^4.0.3",
-        "jiti": "^1.21.0",
-        "lilconfig": "^2.1.0",
-        "micromatch": "^4.0.5",
+        "jiti": "^1.21.6",
+        "lilconfig": "^3.1.3",
+        "micromatch": "^4.0.8",
         "normalize-path": "^3.0.0",
         "object-hash": "^3.0.0",
-        "picocolors": "^1.0.0",
-        "postcss": "^8.4.23",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.47",
         "postcss-import": "^15.1.0",
         "postcss-js": "^4.0.1",
-        "postcss-load-config": "^4.0.1",
-        "postcss-nested": "^6.0.1",
-        "postcss-selector-parser": "^6.0.11",
-        "resolve": "^1.22.2",
-        "sucrase": "^3.32.0"
+        "postcss-load-config": "^4.0.2",
+        "postcss-nested": "^6.2.0",
+        "postcss-selector-parser": "^6.1.2",
+        "resolve": "^1.22.8",
+        "sucrase": "^3.35.0"
       },
       "bin": {
         "tailwind": "lib/cli.js",
@@ -5854,9 +5875,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.4.4",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
-      "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+      "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
       "devOptional": true,
       "bin": {
         "tsc": "bin/tsc",
@@ -5882,9 +5903,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.0.13",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
-      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+      "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
       "dev": true,
       "funding": [
         {
@@ -5901,8 +5922,8 @@
         }
       ],
       "dependencies": {
-        "escalade": "^3.1.1",
-        "picocolors": "^1.0.0"
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.0"
       },
       "bin": {
         "update-browserslist-db": "cli.js"
diff --git a/ui/package.json b/ui/package.json
index 7d569e3..fcf1c55 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -11,21 +11,24 @@
     "dev:cloud": "vite dev --mode=development",
     "build": "npm run build:prod",
     "build:device": "tsc && vite build --mode=device --emptyOutDir",
+    "build:staging": "tsc && vite build --mode=staging",
     "build:prod": "tsc && vite build --mode=production",
-    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
+    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+    "preview": "vite preview"
   },
   "dependencies": {
-    "@headlessui/react": "^2.1.10",
-    "@headlessui/tailwindcss": "^0.2.0",
-    "@heroicons/react": "^2.1.3",
+    "@headlessui/react": "^2.2.0",
+    "@headlessui/tailwindcss": "^0.2.1",
+    "@heroicons/react": "^2.2.0",
     "@xterm/addon-clipboard": "^0.1.0",
     "@xterm/addon-fit": "^0.10.0",
     "@xterm/addon-unicode11": "^0.8.0",
     "@xterm/addon-web-links": "^0.11.0",
     "@xterm/addon-webgl": "^0.18.0",
+    "@xterm/xterm": "^5.5.0",
     "cva": "^1.0.0-beta.1",
     "focus-trap-react": "^10.2.3",
-    "framer-motion": "^11.0.28",
+    "framer-motion": "^11.15.0",
     "lodash.throttle": "^4.1.1",
     "mini-svg-data-uri": "^1.4.4",
     "react": "^18.2.0",
@@ -35,32 +38,33 @@
     "react-icons": "^5.4.0",
     "react-router-dom": "^6.22.3",
     "react-simple-keyboard": "^3.7.112",
-    "recharts": "^2.12.6",
-    "tailwind-merge": "^2.2.2",
+    "react-xtermjs": "^1.0.9",
+    "recharts": "^2.15.0",
+    "tailwind-merge": "^2.5.5",
     "usehooks-ts": "^3.1.0",
     "validator": "^13.12.0",
     "xterm": "^5.3.0",
     "zustand": "^4.5.2"
   },
   "devDependencies": {
-    "@tailwindcss/forms": "^0.5.7",
-    "@tailwindcss/typography": "^0.5.12",
+    "@tailwindcss/forms": "^0.5.9",
+    "@tailwindcss/typography": "^0.5.15",
     "@types/react": "^18.2.66",
     "@types/react-dom": "^18.3.0",
     "@types/validator": "^13.12.2",
     "@typescript-eslint/eslint-plugin": "^7.2.0",
     "@typescript-eslint/parser": "^7.2.0",
-    "@vitejs/plugin-react-swc": "^3.5.0",
-    "autoprefixer": "^10.4.19",
+    "@vitejs/plugin-react-swc": "^3.7.2",
+    "autoprefixer": "^10.4.20",
     "eslint": "^8.57.0",
     "eslint-plugin-react": "^7.34.1",
     "eslint-plugin-react-hooks": "^4.6.0",
     "eslint-plugin-react-refresh": "^0.4.6",
-    "postcss": "^8.4.38",
-    "prettier": "^3.2.5",
+    "postcss": "^8.4.49",
+    "prettier": "^3.4.2",
     "prettier-plugin-tailwindcss": "^0.5.13",
-    "tailwindcss": "^3.4.3",
-    "typescript": "^5.2.2",
+    "tailwindcss": "^3.4.17",
+    "typescript": "^5.7.2",
     "vite": "^5.2.0",
     "vite-tsconfig-paths": "^4.3.2"
   }
diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx
index 13ab896..6558a55 100644
--- a/ui/src/components/ActionBar.tsx
+++ b/ui/src/components/ActionBar.tsx
@@ -2,13 +2,12 @@ import { Button } from "@components/Button";
 import {
   useHidStore,
   useMountMediaStore,
-  useUiStore,
   useSettingsStore,
-  useVideoStore,
+  useUiStore,
 } from "@/hooks/stores";
 import { MdOutlineContentPasteGo } from "react-icons/md";
 import Container from "@components/Container";
-import { LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
+import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
 import { cx } from "@/cva.config";
 import PasteModal from "@/components/popovers/PasteModal";
 import { FaKeyboard } from "react-icons/fa6";
@@ -17,6 +16,7 @@ import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
 import MountPopopover from "./popovers/MountPopover";
 import { Fragment, useCallback, useRef } from "react";
 import { CommandLineIcon } from "@heroicons/react/20/solid";
+import ExtensionPopover from "./popovers/ExtensionPopover";
 
 export default function Actionbar({
   requestFullscreen,
@@ -28,13 +28,12 @@ export default function Actionbar({
   const setVirtualKeyboard = useHidStore(state => state.setVirtualKeyboardEnabled);
   const toggleSidebarView = useUiStore(state => state.toggleSidebarView);
   const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
-  const enableTerminal = useUiStore(state => state.enableTerminal);
-  const setEnableTerminal = useUiStore(state => state.setEnableTerminal);
+  const terminalType = useUiStore(state => state.terminalType);
+  const setTerminalType = useUiStore(state => state.setTerminalType);
   const remoteVirtualMediaState = useMountMediaStore(
     state => state.remoteVirtualMediaState,
   );
   const developerMode = useSettingsStore(state => state.developerMode);
-  const hdmiState = useVideoStore(state => state.hdmiState);
 
   // This is the only way to get a reliable state change for the popover
   // at time of writing this there is no mount, or unmount event for the popover
@@ -55,7 +54,7 @@ export default function Actionbar({
   );
 
   return (
-    <Container className="bg-white border-b border-b-slate-800/20 dark:bg-slate-900 dark:border-b-slate-300/20">
+    <Container className="border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
       <div
         onKeyUp={e => e.stopPropagation()}
         onKeyDown={e => e.stopPropagation()}
@@ -68,7 +67,7 @@ export default function Actionbar({
               theme="light"
               text="Web Terminal"
               LeadingIcon={({ className }) => <CommandLineIcon className={className} />}
-              onClick={() => setEnableTerminal(!enableTerminal)}
+              onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")}
             />
           )}
           <Popover>
@@ -94,7 +93,7 @@ export default function Actionbar({
               {({ open }) => {
                 checkIfStateChanged(open);
                 return (
-                  <div className="w-full max-w-xl mx-auto">
+                  <div className="mx-auto w-full max-w-xl">
                     <PasteModal />
                   </div>
                 );
@@ -136,7 +135,7 @@ export default function Actionbar({
                 {({ open }) => {
                   checkIfStateChanged(open);
                   return (
-                    <div className="w-full max-w-xl mx-auto">
+                    <div className="mx-auto w-full max-w-xl">
                       <MountPopopover />
                     </div>
                   );
@@ -188,7 +187,7 @@ export default function Actionbar({
                 {({ open }) => {
                   checkIfStateChanged(open);
                   return (
-                    <div className="w-full max-w-xl mx-auto">
+                    <div className="mx-auto w-full max-w-xl">
                       <WakeOnLanModal />
                     </div>
                   );
@@ -208,6 +207,33 @@ export default function Actionbar({
         </div>
 
         <div className="flex flex-wrap items-center gap-x-2 gap-y-2">
+          <Popover>
+            <PopoverButton as={Fragment}>
+              <Button
+                size="XS"
+                theme="light"
+                text="Extension"
+                LeadingIcon={LuCable}
+                onClick={() => {
+                  setDisableFocusTrap(true);
+                }}
+              />
+            </PopoverButton>
+            <PopoverPanel
+              anchor="bottom start"
+              transition
+              className={cx(
+                "z-10 flex w-[420px] flex-col !overflow-visible",
+                "flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
+              )}
+            >
+              {({ open }) => {
+                checkIfStateChanged(open);
+                return <ExtensionPopover />;
+              }}
+            </PopoverPanel>
+          </Popover>
+
           <div className="block lg:hidden">
             <Button
               size="XS"
@@ -243,13 +269,12 @@ export default function Actionbar({
               onClick={() => toggleSidebarView("system")}
             />
           </div>
-          <div className="items-center hidden gap-x-2 lg:flex">
+          <div className="hidden items-center gap-x-2 lg:flex">
             <div className="h-4 w-[1px] bg-slate-300 dark:bg-slate-600" />
             <Button
               size="XS"
               theme="light"
               text="Fullscreen"
-              disabled={hdmiState !== 'ready'}
               LeadingIcon={LuMaximize}
               onClick={() => requestFullscreen()}
             />
diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx
index 7bde711..2e9a1fe 100644
--- a/ui/src/components/Button.tsx
+++ b/ui/src/components/Button.tsx
@@ -156,7 +156,16 @@ function ButtonContent(props: ButtonContentPropsType) {
 
 type ButtonPropsType = Pick<
   JSX.IntrinsicElements["button"],
-  "type" | "disabled" | "onClick" | "name" | "value" | "formNoValidate" | "onMouseLeave"
+  | "type"
+  | "disabled"
+  | "onClick"
+  | "name"
+  | "value"
+  | "formNoValidate"
+  | "onMouseLeave"
+  | "onMouseDown"
+  | "onMouseUp"
+  | "onMouseLeave"
 > &
   React.ComponentProps<typeof ButtonContent> & {
     fetcher?: FetcherWithComponents<unknown>;
@@ -179,6 +188,9 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
         type={type}
         disabled={disabled}
         onClick={onClick}
+        onMouseDown={props?.onMouseDown}
+        onMouseUp={props?.onMouseUp}
+        onMouseLeave={props?.onMouseLeave}
         name={props.name}
         value={props.value}
       >
diff --git a/ui/src/components/Terminal.tsx b/ui/src/components/Terminal.tsx
index e910934..9fe3b9c 100644
--- a/ui/src/components/Terminal.tsx
+++ b/ui/src/components/Terminal.tsx
@@ -1,32 +1,172 @@
 import "react-simple-keyboard/build/css/index.css";
-import { useUiStore, useRTCStore } from "@/hooks/stores";
-import { XTerm } from "./Xterm";
+import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores";
 import { Button } from "./Button";
 import { ChevronDownIcon } from "@heroicons/react/16/solid";
-import { cx } from "../cva.config";
-import { Transition } from "@headlessui/react";
+import { cx } from "@/cva.config";
+import { useEffect } from "react";
+import { useXTerm } from "react-xtermjs";
+import { FitAddon } from "@xterm/addon-fit";
+import { WebLinksAddon } from "@xterm/addon-web-links";
+import { WebglAddon } from "@xterm/addon-webgl";
+import { Unicode11Addon } from "@xterm/addon-unicode11";
+import { ClipboardAddon } from "@xterm/addon-clipboard";
 
-function TerminalWrapper() {
-  const enableTerminal = useUiStore(state => state.enableTerminal);
-  const setEnableTerminal = useUiStore(state => state.setEnableTerminal);
-  const terminalChannel = useRTCStore(state => state.terminalChannel);
+const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
+
+// Terminal theme configuration
+const SOLARIZED_THEME = {
+  background: "#0f172a", // Solarized base03
+  foreground: "#839496", // Solarized base0
+  cursor: "#93a1a1", // Solarized base1
+  cursorAccent: "#002b36", // Solarized base03
+  black: "#073642", // Solarized base02
+  red: "#dc322f", // Solarized red
+  green: "#859900", // Solarized green
+  yellow: "#b58900", // Solarized yellow
+  blue: "#268bd2", // Solarized blue
+  magenta: "#d33682", // Solarized magenta
+  cyan: "#2aa198", // Solarized cyan
+  white: "#eee8d5", // Solarized base2
+  brightBlack: "#002b36", // Solarized base03
+  brightRed: "#cb4b16", // Solarized orange
+  brightGreen: "#586e75", // Solarized base01
+  brightYellow: "#657b83", // Solarized base00
+  brightBlue: "#839496", // Solarized base0
+  brightMagenta: "#6c71c4", // Solarized violet
+  brightCyan: "#93a1a1", // Solarized base1
+  brightWhite: "#fdf6e3", // Solarized base3
+} as const;
+
+const TERMINAL_CONFIG = {
+  theme: SOLARIZED_THEME,
+  fontFamily: "'Fira Code', Menlo, Monaco, 'Courier New', monospace",
+  fontSize: 13,
+  allowProposedApi: true,
+  scrollback: 1000,
+  cursorBlink: true,
+  smoothScrollDuration: 100,
+  macOptionIsMeta: true,
+  macOptionClickForcesSelection: true,
+  convertEol: true,
+  linuxMode: false,
+  // Add these configurations:
+  cursorStyle: "block",
+  rendererType: "canvas", // Ensure we're using the canvas renderer
+} as const;
+
+function Terminal({
+  title,
+  dataChannel,
+  type,
+}: {
+  title: string;
+  dataChannel: RTCDataChannel;
+  type: AvailableTerminalTypes;
+}) {
+  const enableTerminal = useUiStore(state => state.terminalType == type);
+  const setTerminalType = useUiStore(state => state.setTerminalType);
+  const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
+
+  const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
+
+  useEffect(() => {
+    setTimeout(() => {
+      setDisableKeyboardFocusTrap(enableTerminal);
+    }, 500);
+
+    return () => {
+      setDisableKeyboardFocusTrap(false);
+    };
+  }, [enableTerminal, instance, ref, setDisableKeyboardFocusTrap, type]);
+
+  const readyState = dataChannel.readyState;
+  useEffect(() => {
+    if (readyState !== "open") return;
+
+    const abortController = new AbortController();
+    dataChannel.addEventListener(
+      "message",
+      e => {
+        instance?.write(new Uint8Array(e.data));
+      },
+      { signal: abortController.signal },
+    );
+
+    const onDataHandler = instance?.onData(data => {
+      dataChannel.send(data);
+    });
+
+    // Setup escape key handler
+    const onKeyHandler = instance?.onKey(e => {
+      const { domEvent } = e;
+      if (domEvent.key === "Escape") {
+        setTerminalType("none");
+        setDisableKeyboardFocusTrap(false);
+        domEvent.preventDefault();
+      }
+    });
+
+    return () => {
+      abortController.abort();
+      onDataHandler?.dispose();
+      onKeyHandler?.dispose();
+    };
+  }, [dataChannel, instance, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
+
+  useEffect(() => {
+    if (!instance) return;
+
+    // Load the fit addon
+    const fitAddon = new FitAddon();
+    instance?.loadAddon(fitAddon);
+
+    instance?.loadAddon(new ClipboardAddon());
+    instance?.loadAddon(new Unicode11Addon());
+    instance?.loadAddon(new WebLinksAddon());
+    instance.unicode.activeVersion = "11";
+
+    if (isWebGl2Supported) {
+      const webGl2Addon = new WebglAddon();
+      webGl2Addon.onContextLoss(() => webGl2Addon.dispose());
+      instance?.loadAddon(webGl2Addon);
+    }
+
+    const handleResize = () => fitAddon.fit();
+
+    // Handle resize event
+    window.addEventListener("resize", handleResize);
+    return () => {
+      window.removeEventListener("resize", handleResize);
+    };
+  }, [ref, instance, dataChannel]);
 
   return (
-    <div onKeyDown={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()}>
-      <Transition show={enableTerminal} appear>
+    <div
+      onKeyDown={e => {
+        e.stopPropagation();
+      }}
+      onKeyUp={e => e.stopPropagation()}
+    >
+      <div>
         <div
-          className={cx([
-            // Base styles
-            "fixed bottom-0 w-full transform transition duration-500 ease-in-out",
-            "translate-y-[0px]",
-            "data-[enter]:translate-y-[500px]",
-            "data-[closed]:translate-y-[500px]",
-          ])}
+          className={cx(
+            [
+              // Base styles
+              "fixed bottom-0 w-full transform transition duration-500 ease-in-out",
+              "translate-y-[0px]",
+            ],
+            {
+              "pointer-events-none translate-y-[500px] opacity-100 transition duration-300":
+                !enableTerminal,
+              "pointer-events-auto translate-y-[0px] opacity-100 transition duration-300":
+                enableTerminal,
+            },
+          )}
         >
           <div className="h-[500px] w-full bg-[#0f172a]">
-            <div className="flex items-center justify-center px-2 py-1 bg-white dark:bg-slate-800 border-y border-y-slate-800/30 dark:border-y-slate-300/20">
+            <div className="flex items-center justify-center border-y border-y-slate-800/30 bg-white px-2 py-1 dark:border-y-slate-300/20 dark:bg-slate-800">
               <h2 className="select-none self-center font-sans text-[12px] text-slate-700 dark:text-slate-300">
-                Web Terminal
+                {title}
               </h2>
               <div className="absolute right-2">
                 <Button
@@ -34,18 +174,19 @@ function TerminalWrapper() {
                   theme="light"
                   text="Hide"
                   LeadingIcon={ChevronDownIcon}
-                  onClick={() => setEnableTerminal(false)}
+                  onClick={() => setTerminalType("none")}
                 />
               </div>
             </div>
+
             <div className="h-[calc(100%-36px)] p-3">
-              <XTerm terminalChannel={terminalChannel} />
+              <div ref={ref} style={{ height: "100%", width: "100%" }} />
             </div>
           </div>
         </div>
-      </Transition>
+      </div>
     </div>
   );
 }
 
-export default TerminalWrapper;
+export default Terminal;
diff --git a/ui/src/components/Xterm.tsx b/ui/src/components/Xterm.tsx
deleted file mode 100644
index 1a0a008..0000000
--- a/ui/src/components/Xterm.tsx
+++ /dev/null
@@ -1,201 +0,0 @@
-import { useEffect, useLayoutEffect, useRef } from "react";
-import { Terminal } from "xterm";
-import { Unicode11Addon } from "@xterm/addon-unicode11";
-import { WebglAddon } from "@xterm/addon-webgl";
-import { WebLinksAddon } from "@xterm/addon-web-links";
-import { FitAddon } from "@xterm/addon-fit";
-import { ClipboardAddon } from "@xterm/addon-clipboard";
-
-import "xterm/css/xterm.css";
-import { useRTCStore, useUiStore } from "../hooks/stores";
-
-const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
-
-// Add this debounce function at the top of the file
-function debounce(func: (...args: any[]) => void, wait: number) {
-  let timeout: number | null = null;
-  return (...args: any[]) => {
-    if (timeout) clearTimeout(timeout);
-    timeout = setTimeout(() => func(...args), wait);
-  };
-}
-
-// Terminal theme configuration
-const SOLARIZED_THEME = {
-  background: "#0f172a", // Solarized base03
-  foreground: "#839496", // Solarized base0
-  cursor: "#93a1a1", // Solarized base1
-  cursorAccent: "#002b36", // Solarized base03
-  black: "#073642", // Solarized base02
-  red: "#dc322f", // Solarized red
-  green: "#859900", // Solarized green
-  yellow: "#b58900", // Solarized yellow
-  blue: "#268bd2", // Solarized blue
-  magenta: "#d33682", // Solarized magenta
-  cyan: "#2aa198", // Solarized cyan
-  white: "#eee8d5", // Solarized base2
-  brightBlack: "#002b36", // Solarized base03
-  brightRed: "#cb4b16", // Solarized orange
-  brightGreen: "#586e75", // Solarized base01
-  brightYellow: "#657b83", // Solarized base00
-  brightBlue: "#839496", // Solarized base0
-  brightMagenta: "#6c71c4", // Solarized violet
-  brightCyan: "#93a1a1", // Solarized base1
-  brightWhite: "#fdf6e3", // Solarized base3
-} as const;
-
-const TERMINAL_CONFIG = {
-  theme: SOLARIZED_THEME,
-  fontFamily: "'Fira Code', Menlo, Monaco, 'Courier New', monospace",
-  fontSize: 13,
-  allowProposedApi: true,
-  scrollback: 1000,
-  cursorBlink: true,
-  smoothScrollDuration: 100,
-  macOptionIsMeta: true,
-  macOptionClickForcesSelection: true,
-  // Add these configurations:
-  convertEol: true,
-  linuxMode: false, // Disable Linux mode which might affect line endings
-} as const;
-
-interface XTermProps {
-  terminalChannel: RTCDataChannel | null;
-}
-
-export function XTerm({ terminalChannel }: XTermProps) {
-  const xtermRef = useRef<Terminal | null>(null);
-  const containerRef = useRef<HTMLDivElement | null>(null);
-  const terminalElmRef = useRef<HTMLDivElement | null>(null);
-  const fitAddonRef = useRef<FitAddon | null>(null);
-  const setEnableTerminal = useUiStore(state => state.setEnableTerminal);
-  const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
-  const peerConnection = useRTCStore(state => state.peerConnection);
-
-  useEffect(() => {
-    setDisableKeyboardFocusTrap(true);
-
-    return () => {
-      setDisableKeyboardFocusTrap(false);
-    };
-  }, [setDisableKeyboardFocusTrap]);
-
-  const initializeTerminalAddons = (term: Terminal) => {
-    const fitAddon = new FitAddon();
-    term.loadAddon(fitAddon);
-    term.loadAddon(new ClipboardAddon());
-    term.loadAddon(new Unicode11Addon());
-    term.loadAddon(new WebLinksAddon());
-    term.unicode.activeVersion = "11";
-
-    if (isWebGl2Supported) {
-      const webGl2Addon = new WebglAddon();
-      webGl2Addon.onContextLoss(() => webGl2Addon.dispose());
-      term.loadAddon(webGl2Addon);
-    }
-
-    return fitAddon;
-  };
-
-  const setupTerminalChannel = (
-    term: Terminal,
-    channel: RTCDataChannel,
-    abortController: AbortController,
-  ) => {
-    channel.onopen = () => {
-      // Handle terminal input
-      term.onData(data => {
-        if (channel.readyState === "open") {
-          channel.send(data);
-        }
-      });
-
-      // Handle terminal output
-      channel.addEventListener(
-        "message",
-        (event: MessageEvent) => {
-          term.write(new Uint8Array(event.data));
-        },
-        { signal: abortController.signal },
-      );
-
-      // Send initial terminal size
-      if (channel.readyState === "open") {
-        channel.send(JSON.stringify({ rows: term.rows, cols: term.cols }));
-      }
-    };
-  };
-
-  useLayoutEffect(() => {
-    if (!terminalElmRef.current) return;
-
-    // Ensure the container has dimensions before initializing
-    if (!terminalElmRef.current.offsetHeight || !terminalElmRef.current.offsetWidth) {
-      return;
-    }
-
-    const term = new Terminal(TERMINAL_CONFIG);
-    const fitAddon = initializeTerminalAddons(term);
-    const abortController = new AbortController();
-
-    // Setup escape key handler
-    term.onKey(e => {
-      const { domEvent } = e;
-      if (domEvent.key === "Escape") {
-        setEnableTerminal(false);
-        setDisableKeyboardFocusTrap(false);
-        domEvent.preventDefault();
-      }
-    });
-
-    let elm: HTMLDivElement | null = terminalElmRef.current;
-    // Initialize terminal
-    setTimeout(() => {
-      if (elm) {
-        console.log("opening terminal");
-        term.open(elm);
-        fitAddon.fit();
-      }
-    }, 800);
-
-    xtermRef.current = term;
-    fitAddonRef.current = fitAddon;
-
-    // Setup resize handling
-    const debouncedResizeHandler = debounce(() => fitAddon.fit(), 100);
-    const resizeObserver = new ResizeObserver(debouncedResizeHandler);
-    resizeObserver.observe(terminalElmRef.current);
-
-    // Focus terminal after a short delay
-    setTimeout(() => {
-      term.focus();
-      terminalElmRef.current?.focus();
-    }, 500);
-
-    // Setup terminal channel if available
-    const channel = peerConnection?.createDataChannel("terminal");
-    if (channel) {
-      setupTerminalChannel(term, channel, abortController);
-    }
-
-    // Cleanup
-    return () => {
-      resizeObserver.disconnect();
-      abortController.abort();
-      term.dispose();
-      elm = null;
-      xtermRef.current = null;
-      fitAddonRef.current = null;
-    };
-  }, [peerConnection, setDisableKeyboardFocusTrap, setEnableTerminal, terminalChannel]);
-
-  return (
-    <div className="w-full h-full" ref={containerRef}>
-      <div
-        className="w-full h-full terminal-container"
-        ref={terminalElmRef}
-        style={{ display: "flex", minHeight: "100%" }}
-      ></div>
-    </div>
-  );
-}
diff --git a/ui/src/components/extensions/ATXPowerControl.tsx b/ui/src/components/extensions/ATXPowerControl.tsx
new file mode 100644
index 0000000..2d1323f
--- /dev/null
+++ b/ui/src/components/extensions/ATXPowerControl.tsx
@@ -0,0 +1,171 @@
+import { Button } from "@components/Button";
+import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
+import Card from "@components/Card";
+import { SectionHeader } from "@components/SectionHeader";
+import { useEffect, useState } from "react";
+import notifications from "@/notifications";
+import { useJsonRpc } from "../../hooks/useJsonRpc";
+import LoadingSpinner from "../LoadingSpinner";
+
+const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
+
+interface ATXState {
+  power: boolean;
+  hdd: boolean;
+}
+
+export function ATXPowerControl() {
+  const [isPowerPressed, setIsPowerPressed] = useState(false);
+  const [powerPressTimer, setPowerPressTimer] = useState<ReturnType<
+    typeof setTimeout
+  > | null>(null);
+  const [atxState, setAtxState] = useState<ATXState | null>(null);
+
+  const [send] = useJsonRpc(function onRequest(resp) {
+    if (resp.method === "atxState") {
+      setAtxState(resp.params as ATXState);
+    }
+  });
+
+  // Request initial state
+  useEffect(() => {
+    send("getATXState", {}, resp => {
+      if ("error" in resp) {
+        notifications.error(
+          `Failed to get ATX state: ${resp.error.data || "Unknown error"}`,
+        );
+        return;
+      }
+      setAtxState(resp.result as ATXState);
+    });
+  }, [send]);
+
+  const handlePowerPress = (pressed: boolean) => {
+    // Prevent phantom releases
+    if (!pressed && !isPowerPressed) return;
+
+    setIsPowerPressed(pressed);
+
+    // Handle button press
+    if (pressed) {
+      // Start long press timer
+      const timer = setTimeout(() => {
+        // Send long press action
+        console.log("Sending long press ATX power action");
+        send("setATXPowerAction", { action: "power-long" }, resp => {
+          if ("error" in resp) {
+            notifications.error(
+              `Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
+            );
+          }
+          setIsPowerPressed(false);
+        });
+      }, LONG_PRESS_DURATION);
+
+      setPowerPressTimer(timer);
+    }
+    // Handle button release
+    else {
+      // If timer exists, was a short press
+      if (powerPressTimer) {
+        clearTimeout(powerPressTimer);
+        setPowerPressTimer(null);
+
+        // Send short press action
+        console.log("Sending short press ATX power action");
+        send("setATXPowerAction", { action: "power-short" }, resp => {
+          if ("error" in resp) {
+            notifications.error(
+              `Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
+            );
+          }
+        });
+      }
+    }
+  };
+
+  // Cleanup timer on unmount
+  useEffect(() => {
+    return () => {
+      if (powerPressTimer) {
+        clearTimeout(powerPressTimer);
+      }
+    };
+  }, [powerPressTimer]);
+
+  return (
+    <div className="space-y-4">
+      <SectionHeader
+        title="ATX Power Control"
+        description="Control your ATX power settings"
+      />
+
+      {atxState === null ? (
+        <Card className="flex h-[120px] items-center justify-center p-3">
+          <LoadingSpinner className="w-6 h-6 text-blue-500 dark:text-blue-400" />
+        </Card>
+      ) : (
+        <Card className="h-[120px] animate-fadeIn opacity-0">
+          <div className="p-3 space-y-4">
+            {/* Control Buttons */}
+            <div className="flex items-center space-x-2">
+              <Button
+                size="SM"
+                theme="light"
+                LeadingIcon={LuPower}
+                text="Power"
+                onMouseDown={() => handlePowerPress(true)}
+                onMouseUp={() => handlePowerPress(false)}
+                onMouseLeave={() => handlePowerPress(false)}
+                className={isPowerPressed ? "opacity-75" : ""}
+              />
+              <Button
+                size="SM"
+                theme="light"
+                LeadingIcon={LuRotateCcw}
+                text="Reset"
+                onClick={() => {
+                  send("setATXPowerAction", { action: "reset" }, resp => {
+                    if ("error" in resp) {
+                      notifications.error(
+                        `Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
+                      );
+                      return;
+                    }
+                  });
+                }}
+              />
+            </div>
+
+            <hr className="border-slate-700/30 dark:border-slate-600/30" />
+            {/* Status Indicators */}
+            <div className="flex items-center space-x-4">
+              <div className="flex items-center space-x-2">
+                <span className="text-sm text-slate-600 dark:text-slate-400">
+                  <LuPower
+                    strokeWidth={3}
+                    className={`mr-1 inline ${
+                      atxState?.power ? "text-green-600" : "text-slate-300"
+                    }`}
+                  />
+                  Power LED
+                </span>
+              </div>
+              <div className="flex items-center space-x-2">
+                <span className="text-sm text-slate-600 dark:text-slate-400">
+                  <LuHardDrive
+                    strokeWidth={3}
+                    className={`mr-1 inline ${
+                      atxState?.hdd ? "text-blue-400" : "text-slate-300"
+                    }`}
+                  />
+                  HDD LED
+                </span>
+              </div>
+            </div>
+          </div>
+        </Card>
+      )}
+    </div>
+  );
+}
diff --git a/ui/src/components/extensions/DCPowerControl.tsx b/ui/src/components/extensions/DCPowerControl.tsx
new file mode 100644
index 0000000..e4ba29d
--- /dev/null
+++ b/ui/src/components/extensions/DCPowerControl.tsx
@@ -0,0 +1,114 @@
+import { Button } from "@components/Button";
+import { LuPower } from "react-icons/lu";
+import Card from "@components/Card";
+import { SectionHeader } from "@components/SectionHeader";
+import FieldLabel from "../FieldLabel";
+import { useJsonRpc } from "@/hooks/useJsonRpc";
+import { useCallback, useEffect, useState } from "react";
+import notifications from "@/notifications";
+import LoadingSpinner from "../LoadingSpinner";
+
+interface DCPowerState {
+  isOn: boolean;
+  voltage: number;
+  current: number;
+  power: number;
+}
+
+export function DCPowerControl() {
+  const [send] = useJsonRpc();
+  const [powerState, setPowerState] = useState<DCPowerState | null>(null);
+
+  const getDCPowerState = useCallback(() => {
+    send("getDCPowerState", {}, resp => {
+      if ("error" in resp) {
+        notifications.error(
+          `Failed to get DC power state: ${resp.error.data || "Unknown error"}`,
+        );
+        return;
+      }
+      setPowerState(resp.result as DCPowerState);
+    });
+  }, [send]);
+
+  const handlePowerToggle = (enabled: boolean) => {
+    send("setDCPowerState", { enabled }, resp => {
+      if ("error" in resp) {
+        notifications.error(
+          `Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
+        );
+        return;
+      }
+      getDCPowerState(); // Refresh state after change
+    });
+  };
+
+  useEffect(() => {
+    getDCPowerState();
+    // Set up polling interval to update status
+    const interval = setInterval(getDCPowerState, 1000);
+    return () => clearInterval(interval);
+  }, [getDCPowerState]);
+
+  return (
+    <div className="space-y-4">
+      <SectionHeader
+        title="DC Power Control"
+        description="Control your DC power settings"
+      />
+
+      {powerState === null ? (
+        <Card className="flex h-[160px] justify-center p-3">
+          <LoadingSpinner className="w-6 h-6 text-blue-500 dark:text-blue-400" />
+        </Card>
+      ) : (
+        <Card className="h-[160px] animate-fadeIn opacity-0">
+          <div className="p-3 space-y-4">
+            {/* Power Controls */}
+            <div className="flex items-center space-x-2">
+              <Button
+                size="SM"
+                theme="light"
+                LeadingIcon={LuPower}
+                text="Power On"
+                onClick={() => handlePowerToggle(true)}
+                disabled={powerState.isOn}
+              />
+              <Button
+                size="SM"
+                theme="light"
+                LeadingIcon={LuPower}
+                text="Power Off"
+                disabled={!powerState.isOn}
+                onClick={() => handlePowerToggle(false)}
+              />
+            </div>
+            <hr className="border-slate-700/30 dark:border-slate-600/30" />
+
+            {/* Status Display */}
+            <div className="grid grid-cols-3 gap-4">
+              <div className="space-y-1">
+                <FieldLabel label="Voltage" />
+                <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
+                  {powerState.voltage.toFixed(1)}V
+                </p>
+              </div>
+              <div className="space-y-1">
+                <FieldLabel label="Current" />
+                <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
+                  {powerState.current.toFixed(1)}A
+                </p>
+              </div>
+              <div className="space-y-1">
+                <FieldLabel label="Power" />
+                <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
+                  {powerState.power.toFixed(1)}W
+                </p>
+              </div>
+            </div>
+          </div>
+        </Card>
+      )}
+    </div>
+  );
+}
diff --git a/ui/src/components/extensions/SerialConsole.tsx b/ui/src/components/extensions/SerialConsole.tsx
new file mode 100644
index 0000000..c57d364
--- /dev/null
+++ b/ui/src/components/extensions/SerialConsole.tsx
@@ -0,0 +1,130 @@
+import { Button } from "@components/Button";
+import { LuTerminal } from "react-icons/lu";
+import Card from "@components/Card";
+import { SectionHeader } from "@components/SectionHeader";
+import { SelectMenuBasic } from "../SelectMenuBasic";
+import { useJsonRpc } from "@/hooks/useJsonRpc";
+import { useEffect, useState } from "react";
+import notifications from "@/notifications";
+import { useUiStore } from "@/hooks/stores";
+
+interface SerialSettings {
+  baudRate: string;
+  dataBits: string;
+  stopBits: string;
+  parity: string;
+}
+
+export function SerialConsole() {
+  const [send] = useJsonRpc();
+  const [settings, setSettings] = useState<SerialSettings>({
+    baudRate: "9600",
+    dataBits: "8",
+    stopBits: "1",
+    parity: "none",
+  });
+
+  useEffect(() => {
+    send("getSerialSettings", {}, resp => {
+      if ("error" in resp) {
+        notifications.error(
+          `Failed to get serial settings: ${resp.error.data || "Unknown error"}`,
+        );
+        return;
+      }
+      setSettings(resp.result as SerialSettings);
+    });
+  }, [send]);
+
+  const handleSettingChange = (setting: keyof SerialSettings, value: string) => {
+    const newSettings = { ...settings, [setting]: value };
+    send("setSerialSettings", { settings: newSettings }, resp => {
+      if ("error" in resp) {
+        notifications.error(
+          `Failed to update serial settings: ${resp.error.data || "Unknown error"}`,
+        );
+        return;
+      }
+      setSettings(newSettings);
+    });
+  };
+  const setTerminalType = useUiStore(state => state.setTerminalType);
+
+  return (
+    <div className="space-y-4">
+      <SectionHeader
+        title="Serial Console"
+        description="Configure your serial console settings"
+      />
+
+      <Card className="animate-fadeIn opacity-0">
+        <div className="space-y-4 p-3">
+          {/* Open Console Button */}
+          <div className="flex items-center">
+            <Button
+              size="SM"
+              theme="primary"
+              LeadingIcon={LuTerminal}
+              text="Open Console"
+              onClick={() => {
+                setTerminalType("serial");
+                console.log("Opening serial console with settings: ", settings);
+              }}
+            />
+          </div>
+          <hr className="border-slate-700/30 dark:border-slate-600/30" />
+          {/* Settings */}
+          <div className="grid grid-cols-2 gap-4">
+            <SelectMenuBasic
+              label="Baud Rate"
+              options={[
+                { label: "1200", value: "1200" },
+                { label: "2400", value: "2400" },
+                { label: "4800", value: "4800" },
+                { label: "9600", value: "9600" },
+                { label: "19200", value: "19200" },
+                { label: "38400", value: "38400" },
+                { label: "57600", value: "57600" },
+                { label: "115200", value: "115200" },
+              ]}
+              value={settings.baudRate}
+              onChange={e => handleSettingChange("baudRate", e.target.value)}
+            />
+
+            <SelectMenuBasic
+              label="Data Bits"
+              options={[
+                { label: "8", value: "8" },
+                { label: "7", value: "7" },
+              ]}
+              value={settings.dataBits}
+              onChange={e => handleSettingChange("dataBits", e.target.value)}
+            />
+
+            <SelectMenuBasic
+              label="Stop Bits"
+              options={[
+                { label: "1", value: "1" },
+                { label: "1.5", value: "1.5" },
+                { label: "2", value: "2" },
+              ]}
+              value={settings.stopBits}
+              onChange={e => handleSettingChange("stopBits", e.target.value)}
+            />
+
+            <SelectMenuBasic
+              label="Parity"
+              options={[
+                { label: "None", value: "none" },
+                { label: "Even", value: "even" },
+                { label: "Odd", value: "odd" },
+              ]}
+              value={settings.parity}
+              onChange={e => handleSettingChange("parity", e.target.value)}
+            />
+          </div>
+        </div>
+      </Card>
+    </div>
+  );
+}
diff --git a/ui/src/components/popovers/ExtensionPopover.tsx b/ui/src/components/popovers/ExtensionPopover.tsx
new file mode 100644
index 0000000..9438bdb
--- /dev/null
+++ b/ui/src/components/popovers/ExtensionPopover.tsx
@@ -0,0 +1,145 @@
+import { useEffect, useState } from "react";
+import { useJsonRpc } from "@/hooks/useJsonRpc";
+import Card, { GridCard } from "@components/Card";
+import { SectionHeader } from "@components/SectionHeader";
+import { Button } from "../Button";
+import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
+import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
+import { DCPowerControl } from "@components/extensions/DCPowerControl";
+import { SerialConsole } from "@components/extensions/SerialConsole";
+import notifications from "../../notifications";
+
+interface Extension {
+  id: string;
+  name: string;
+  description: string;
+  icon: any;
+}
+
+const AVAILABLE_EXTENSIONS: Extension[] = [
+  {
+    id: "atx-power",
+    name: "ATX Power Control",
+    description: "Control your ATX Power extension",
+    icon: LuPower,
+  },
+  {
+    id: "dc-power",
+    name: "DC Power Control",
+    description: "Control your DC Power extension",
+    icon: LuPlugZap,
+  },
+  {
+    id: "serial-console",
+    name: "Serial Console",
+    description: "Access your serial console extension",
+    icon: LuTerminal,
+  },
+];
+
+export default function ExtensionPopover() {
+  const [send] = useJsonRpc();
+  const [activeExtension, setActiveExtension] = useState<Extension | null>(null);
+
+  // Load active extension on component mount
+  useEffect(() => {
+    send("getActiveExtension", {}, resp => {
+      if ("error" in resp) return;
+      const extensionId = resp.result as string;
+      if (extensionId) {
+        const extension = AVAILABLE_EXTENSIONS.find(ext => ext.id === extensionId);
+        if (extension) {
+          setActiveExtension(extension);
+        }
+      }
+    });
+  }, [send]);
+
+  const handleSetActiveExtension = (extension: Extension | null) => {
+    send("setActiveExtension", { extensionId: extension?.id || "" }, resp => {
+      if ("error" in resp) {
+        notifications.error(`Failed to set active extension: ${resp.error.data || "Unknown error"}`);
+        return;
+      }
+      setActiveExtension(extension);
+    });
+  };
+
+  const renderActiveExtension = () => {
+    switch (activeExtension?.id) {
+      case "atx-power":
+        return <ATXPowerControl />;
+      case "dc-power":
+        return <DCPowerControl />;
+      case "serial-console":
+        return <SerialConsole />;
+      default:
+        return null;
+    }
+  };
+
+  return (
+    <GridCard>
+      <div className="p-4 py-3 space-y-4">
+        <div className="grid h-full grid-rows-headerBody">
+          <div className="space-y-4">
+            {activeExtension ? (
+              // Extension Control View
+              <div className="space-y-4">
+                {renderActiveExtension()}
+
+                <div
+                  className="flex items-center justify-end space-x-2 opacity-0 animate-fadeIn"
+                  style={{
+                    animationDuration: "0.7s",
+                    animationDelay: "0.2s",
+                  }}
+                >
+                  <Button
+                    size="SM"
+                    theme="light"
+                    text="Unload Extension"
+                    onClick={() => handleSetActiveExtension(null)}
+                  />
+                </div>
+              </div>
+            ) : (
+              // Extensions List View
+              <div className="space-y-4">
+                <SectionHeader
+                  title="Extensions"
+                  description="Load and manage your extensions"
+                />
+                <Card className="opacity-0 animate-fadeIn">
+                  <div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
+                    {AVAILABLE_EXTENSIONS.map(extension => (
+                      <div
+                        key={extension.id}
+                        className="flex items-center justify-between p-3"
+                      >
+                        <div className="space-y-0.5">
+                          <p className="text-sm font-semibold leading-none text-slate-900 dark:text-slate-100">
+                            {extension.name}
+                          </p>
+                          <p className="text-sm text-slate-600 dark:text-slate-400">
+                            {extension.description}
+                          </p>
+                        </div>
+                        <Button
+                          size="XS"
+                          theme="light"
+                          text="Load"
+                          onClick={() => handleSetActiveExtension(extension)}
+                        />
+                      </div>
+                    ))}
+                  </div>
+                </Card>
+              </div>
+            )}
+          </div>
+        </div>
+      </div>
+    </GridCard>
+  );
+}
diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts
index 7a0268b..5b1366c 100644
--- a/ui/src/hooks/stores.ts
+++ b/ui/src/hooks/stores.ts
@@ -22,6 +22,7 @@ const appendStatToMap = <T extends { timestamp: number }>(
 // Constants and types
 export type AvailableSidebarViews = "system" | "connection-stats";
 export type AvailableModalViews = "connection-stats" | "settings";
+export type AvailableTerminalTypes = "kvm" | "serial" | "none";
 
 export interface User {
   sub: string;
@@ -52,13 +53,13 @@ interface UIState {
   isAttachedVirtualKeyboardVisible: boolean;
   setAttachedVirtualKeyboardVisibility: (enabled: boolean) => void;
 
-  enableTerminal: boolean;
-  setEnableTerminal: (enabled: UIState["enableTerminal"]) => void;
+  terminalType: AvailableTerminalTypes;
+  setTerminalType: (enabled: UIState["terminalType"]) => void;
 }
 
 export const useUiStore = create<UIState>(set => ({
-  enableTerminal: false,
-  setEnableTerminal: enabled => set({ enableTerminal: enabled }),
+  terminalType: "none",
+  setTerminalType: type => set({ terminalType: type }),
 
   sidebarView: null,
   setSidebarView: view => set({ sidebarView: view }),
diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx
index bd950b2..10ae0bc 100644
--- a/ui/src/routes/devices.$id.tsx
+++ b/ui/src/routes/devices.$id.tsx
@@ -5,12 +5,12 @@ import {
   HidState,
   UpdateState,
   useHidStore,
+  useMountMediaStore,
   User,
   useRTCStore,
   useUiStore,
   useUpdateStore,
   useVideoStore,
-  useMountMediaStore,
   VideoState,
 } from "@/hooks/stores";
 import WebRTCVideo from "@components/WebRTCVideo";
@@ -35,7 +35,7 @@ import api from "../api";
 import { DeviceStatus } from "./welcome-local";
 import FocusTrap from "focus-trap-react";
 import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal";
-import TerminalWrapper from "../components/Terminal";
+import Terminal from "@components/Terminal";
 import { CLOUD_API, SIGNAL_API } from "@/ui.config";
 
 interface LocalLoaderResp {
@@ -328,6 +328,7 @@ export default function KvmIdRoute() {
   const setHdmiState = useVideoStore(state => state.setHdmiState);
 
   const [hasUpdated, setHasUpdated] = useState(false);
+
   function onJsonRpcRequest(resp: JsonRpcRequest) {
     if (resp.method === "otherSessionConnected") {
       console.log("otherSessionConnected", resp.params);
@@ -413,10 +414,39 @@ export default function KvmIdRoute() {
 
   // System update
   const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
+
+  const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
+  const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
+
+  useEffect(() => {
+    if (!peerConnection) return;
+    if (!kvmTerminal) {
+      console.log('Creating data channel "terminal"');
+      setKvmTerminal(peerConnection.createDataChannel("terminal"));
+    }
+
+    if (!serialConsole) {
+      console.log('Creating data channel "serial"');
+      setSerialConsole(peerConnection.createDataChannel("serial"));
+    }
+  }, [kvmTerminal, peerConnection, serialConsole]);
+
+  useEffect(() => {
+    kvmTerminal?.addEventListener("message", e => {
+      console.log(e.data);
+    });
+
+    return () => {
+      kvmTerminal?.removeEventListener("message", e => {
+        console.log(e.data);
+      });
+    };
+  }, [kvmTerminal]);
+
   return (
     <>
       <Transition show={!isUpdateDialogOpen && otaState.updating}>
-        <div className="fixed inset-0 z-10 flex items-start justify-center w-full h-full max-w-xl mx-auto translate-y-8 pointer-events-none">
+        <div className="pointer-events-none fixed inset-0 z-10 mx-auto flex h-full w-full max-w-xl translate-y-8 items-start justify-center">
           <div className="transition duration-1000 ease-in data-[closed]:opacity-0">
             <UpdateInProgressStatusCard
               setIsUpdateDialogOpen={setIsUpdateDialogOpen}
@@ -425,7 +455,6 @@ export default function KvmIdRoute() {
           </div>
         </div>
       </Transition>
-
       <div className="relative h-full">
         <FocusTrap
           paused={disableKeyboardFocusTrap}
@@ -459,9 +488,7 @@ export default function KvmIdRoute() {
       <OtherSessionConnectedModal
         open={isOtherSessionConnectedModalOpen}
         setOpen={state => {
-          if (state === false) {
-            connectWebRTC();
-          }
+          if (!state) connectWebRTC().then(r => r);
 
           // It takes some time for the WebRTC connection to be established, so we wait a bit before closing the modal
           setTimeout(() => {
@@ -469,7 +496,12 @@ export default function KvmIdRoute() {
           }, 1000);
         }}
       />
-      <TerminalWrapper />
+      {kvmTerminal && (
+        <Terminal type="kvm" dataChannel={kvmTerminal} title="KVM Terminal" />
+      )}
+      {serialConsole && (
+        <Terminal type="serial" dataChannel={serialConsole} title="Serial Console" />
+      )}
     </>
   );
 }
diff --git a/webrtc.go b/webrtc.go
index 27084fc..5e9ce3d 100644
--- a/webrtc.go
+++ b/webrtc.go
@@ -113,6 +113,8 @@ func newSession(config SessionConfig) (*Session, error) {
 			d.OnMessage(onDiskMessage)
 		case "terminal":
 			handleTerminalChannel(d)
+		case "serial":
+			handleSerialChannel(d)
 		default:
 			if strings.HasPrefix(d.Label(), uploadIdPrefix) {
 				go handleUploadChannel(d)