diff --git a/.gitlab/ci/.build-binaries.yml b/.gitlab/ci/.build-binaries.yml
index 174203fdcd52622712b38ef0718612d6cf873a94..4877cd1697d13de412485f281f2bf2b46a8b1019 100644
--- a/.gitlab/ci/.build-binaries.yml
+++ b/.gitlab/ci/.build-binaries.yml
@@ -16,5 +16,6 @@ build-all-binaries:
           - artifacts/venv-manager
           - artifacts/inventory-manager
           - artifacts/plugin-registry
+          - artifacts/react-ui
         expire_in: 1 week
     <<: *build-binaries
diff --git a/.gitlab/ci/.build-container-images.yml b/.gitlab/ci/.build-container-images.yml
index da70dcba2d6ae122b967c2417c014750cbed26b0..7fda52178317dd8ff0d4a0bcc0f6eda809addf8a 100644
--- a/.gitlab/ci/.build-container-images.yml
+++ b/.gitlab/ci/.build-container-images.yml
@@ -75,6 +75,16 @@ build-inventory-manager-image:
         - docker push "$INVENTORY_MANAGER_IMAGE_NAME:$CI_COMMIT_REF_SLUG"
     <<: *build
 
+build-react-ui-image:
+    script:
+        - REACT_UI_IMAGE_NAME="${CI_REGISTRY_IMAGE}/react-ui"
+        - docker buildx build -t "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA" -f "${CI_PROJECT_DIR}/react-ui/docker/webserver/Dockerfile" .
+        - docker push "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA"
+        - docker tag "$REACT_UI_IMAGE_NAME:$CI_COMMIT_SHA" "$REACT_UI_IMAGE_NAME:$CI_COMMIT_REF_SLUG"
+        - docker push "$REACT_UI_IMAGE_NAME:$CI_COMMIT_REF_SLUG"
+    <<: *build
+    
+
 build-integration-test-images:
     needs: ["build-controller-image"]
     script:
diff --git a/.gitlab/ci/.code-quality-ci.yml b/.gitlab/ci/.code-quality-ci.yml
index d054508c42f8f2a9bca5020a40c09aebd06516d5..95ed3abe96e1220d751c05e5e16224a29e47b99d 100644
--- a/.gitlab/ci/.code-quality-ci.yml
+++ b/.gitlab/ci/.code-quality-ci.yml
@@ -1,5 +1,5 @@
 code-quality:
-    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.62.2-alpine
+    image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/golangci/golangci-lint:v1.63.4-alpine
     stage: analyze
     script:
         # writes golangci-lint output to gl-code-quality-report.json
diff --git a/.gitlab/ci/.react-ui.yml b/.gitlab/ci/.react-ui.yml
deleted file mode 100644
index 873b694c892b39870eb60ad629f836055089734b..0000000000000000000000000000000000000000
--- a/.gitlab/ci/.react-ui.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-build-react-ui:
-  stage: build
-  
\ No newline at end of file
diff --git a/.gitlab/ci/.renovate.yml b/.gitlab/ci/.renovate.yml
index 55c8ea3356625716b25ede02abb096628ee318cf..81070e9da254d8199c4a375c34c1bfcd3e62d710 100644
--- a/.gitlab/ci/.renovate.yml
+++ b/.gitlab/ci/.renovate.yml
@@ -1,7 +1,7 @@
 renovate:
     stage: tools
 
-    image: renovate/renovate:39.48.1
+    image: renovate/renovate:39.107.0
 
     variables:
         LOG_LEVEL: debug
diff --git a/.golangci.yml b/.golangci.yml
index cdd404e55c3ed172a14d18e70ece0ec4be7ebe53..42bb6ac6b31f1abf94ecc1e47bcf910873a99dc3 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -14,13 +14,13 @@ output:
       - format: colored-line-number
     print-issued-lines: true
     print-linter-name: true
-    uniq-by-line: true
     path-prefix: ""
 
 issues:
     exclude-use-default: false
     max-issues-per-linter: 0
     max-same-issues: 0
+    uniq-by-line: true
     exclude-files:
         - http.go
     # directories to be ignored by linters
diff --git a/Makefile b/Makefile
index 888644abed5135d8bf2364302499eefd2f2ea0e4..693329772ec396aada21e43a6586306b8949af60 100644
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,7 @@ PLUGIN_NAME= bundled_plugin.zip
 
 # Tool Versions
 GOTESTSUM_VERSION=v1.8.1
-GOLANGCI_LINT_VERSION=v1.62.0
+GOLANGCI_LINT_VERSION=v1.63.4
 MOCKERY_VERSION=v2.20.0
 YGOT_GENERATOR_VERSION=v0.27.0
 YGOT_GENERATOR_GENERATOR_VERSION=v0.0.4
@@ -60,7 +60,7 @@ lint-fix: install-tools
 
 build: pre build-gosdn build-gosdnc build-plugin-registry build-venv-manager build-arista-routing-engine-app build-hostname-checker-app build-basic-interface-monitoring-app build-inventory-manager
 
-containerize-all: containerize-gosdn containerize-gosdnc containerize-plugin-registry containerize-venv-manager containerize-arista-routing-engine-app containerize-inventory-manager
+containerize-all: containerize-gosdn containerize-gosdnc containerize-plugin-registry containerize-venv-manager containerize-arista-routing-engine-app containerize-inventory-manager containerize-react-ui
 
 generate-all-certs: pre generate-root-ca generate-gosdn-certs generate-gnmi-target-certs
 
diff --git a/controller/northbound/server/auth_test.go b/controller/northbound/server/auth_test.go
index e5c9e7389d443c0f653f53a3c5f32966f5c2671d..956ef2be19b5da9be5eebe2636763bb7c91fe7b5 100644
--- a/controller/northbound/server/auth_test.go
+++ b/controller/northbound/server/auth_test.go
@@ -89,7 +89,13 @@ func TestAuth_Login(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("username"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("username"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				}},
@@ -160,7 +166,13 @@ func TestAuth_Logout(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("username"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("username"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				}},
diff --git a/controller/northbound/server/role_test.go b/controller/northbound/server/role_test.go
index bb787a4160530b0454d4b2c0198f47c0f9a98ffc..2d75e067843f15c594d6d34e2b0a503d058c8582 100644
--- a/controller/northbound/server/role_test.go
+++ b/controller/northbound/server/role_test.go
@@ -84,10 +84,20 @@ func TestRole_CreateRoles(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("roles[0].name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("roles"),
+							},
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 3 characters"),
-				}},
+				},
+			},
 		},
 		{
 			name: "role with too short description should fail",
@@ -105,7 +115,16 @@ func TestRole_CreateRoles(t *testing.T) {
 			want:    &apb.CreateRolesResponse{},
 			wantErr: true,
 			validationErrors: []*validate.Violation{{
-				FieldPath:    stringToPointer("roles[0].description"),
+				Field: &validate.FieldPath{
+					Elements: []*validate.FieldPathElement{
+						{
+							FieldName: stringToPointer("roles"),
+						},
+						{
+							FieldName: stringToPointer("description"),
+						},
+					},
+				},
 				ConstraintId: stringToPointer("string.min_len"),
 				Message:      stringToPointer("value length must be at least 3 characters"),
 			}},
@@ -181,7 +200,13 @@ func TestRole_GetRole(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("role_name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("role_name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
@@ -355,7 +380,16 @@ func TestRole_UpdateRoles(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("roles[0].name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("roles"),
+							},
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 3 characters"),
 				},
@@ -379,7 +413,16 @@ func TestRole_UpdateRoles(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("roles[0].description"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("roles"),
+							},
+							{
+								FieldName: stringToPointer("description"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 3 characters"),
 				},
@@ -456,12 +499,24 @@ func TestRole_DeletePermissionsForRole(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("role_name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("role_name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
 				{
-					FieldPath:    stringToPointer("permissions_to_delete"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("permissions_to_delete"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
diff --git a/controller/northbound/server/topology_test.go b/controller/northbound/server/topology_test.go
index 44ce77b57b19c31d5cc6292e5af5c2ac265e3a42..dea560a452ff02ff710d6ca71fcc251d8f57472d 100644
--- a/controller/northbound/server/topology_test.go
+++ b/controller/northbound/server/topology_test.go
@@ -285,27 +285,72 @@ func TestTopology_AddLink(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("link.name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 1 characters"),
 				},
 				{
-					FieldPath:    stringToPointer("link.sourceNode"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("sourceNode"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
 				{
-					FieldPath:    stringToPointer("link.targetNode"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("targetNode"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
 				{
-					FieldPath:    stringToPointer("link.sourcePort"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("sourcePort"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
 				{
-					FieldPath:    stringToPointer("link.targetPort"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("link"),
+							},
+							{
+								FieldName: stringToPointer("targetPort"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				},
@@ -461,7 +506,13 @@ func TestTopology_DeleteLink(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("id"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("id"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				}},
diff --git a/controller/northbound/server/user_test.go b/controller/northbound/server/user_test.go
index 4a1c663279ce5607a40ff3dd1fd8d87f03d7e0bf..9fcdf4d610cb5685e69e231bd2d65cde95a7e9e8 100644
--- a/controller/northbound/server/user_test.go
+++ b/controller/northbound/server/user_test.go
@@ -90,7 +90,16 @@ func TestUser_CreateUsers(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("user[0].password"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("user"),
+							},
+							{
+								FieldName: stringToPointer("password"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 5 characters"),
 				}},
@@ -116,7 +125,16 @@ func TestUser_CreateUsers(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("user[0].name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("user"),
+							},
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("string.min_len"),
 					Message:      stringToPointer("value length must be at least 3 characters"),
 				}},
@@ -190,7 +208,13 @@ func TestUser_GetUser(t *testing.T) {
 			wantErr: true,
 			validationErrors: []*validate.Violation{
 				{
-					FieldPath:    stringToPointer("name"),
+					Field: &validate.FieldPath{
+						Elements: []*validate.FieldPathElement{
+							{
+								FieldName: stringToPointer("name"),
+							},
+						},
+					},
 					ConstraintId: stringToPointer("required"),
 					Message:      stringToPointer("value is required"),
 				}},
diff --git a/controller/northbound/server/utils_test.go b/controller/northbound/server/utils_test.go
index 7a4636caabf0f8d66d6e36383e59026d2f376315..dd95d8f2e8c56ba51bf01ade6d6cf5efa0831758 100644
--- a/controller/northbound/server/utils_test.go
+++ b/controller/northbound/server/utils_test.go
@@ -23,7 +23,8 @@ func isEqualFieldPaths(violationFieldPath, errFieldPath *validate.FieldPath) boo
 	}
 
 	for i, elem := range violationFieldPath.GetElements() {
-		if elem != errFieldPath.GetElements()[i] {
+		errElem := errFieldPath.GetElements()[i]
+		if *elem.FieldName != *errElem.FieldName {
 			return false
 		}
 	}
diff --git a/controller/nucleus/networkElementWatcher.go b/controller/nucleus/networkElementWatcher.go
index 8b542d086ca5c61e7fb5d776137c6c11260fd858..07ad775f253c361a7921c6699d95b2681c1e7002 100644
--- a/controller/nucleus/networkElementWatcher.go
+++ b/controller/nucleus/networkElementWatcher.go
@@ -200,6 +200,8 @@ func (n *NetworkElementWatcher) StopAndRemoveNetworkElementSubscription(subID uu
 // handleSubscribeResponse takes the subscribe response and additional information about the network element to distinguish
 // from which network element a subscribe response was sent including improved error handling.
 func (n *NetworkElementWatcher) handleSubscribeResponse(subscriptionInfo *transport.SubscriptionInformation, workerName string) {
+	log.Debugf("Received Subscribe response: MNE ID: %s, MNE Name: %s, SubResponse: %v", subscriptionInfo.NetworkElementID, subscriptionInfo.NetworkElementName, subscriptionInfo.SubResponse)
+
 	if subscriptionInfo.SubResponse == nil {
 		// Note: This needs proper error handling, no idea how yet. Simply logging would lead to spam in the console
 		// if the target that was subscribed to is not reachable anymore.
@@ -232,6 +234,11 @@ func (n *NetworkElementWatcher) handleSubscribeResponse(subscriptionInfo *transp
 func (n *NetworkElementWatcher) handleSubscribeResponseUpdate(resp *gpb.SubscribeResponse_Update, subscriptionInfo *transport.SubscriptionInformation) {
 	pathsAndValues := make(map[string]string, len(resp.Update.Update))
 
+	if resp.Update == nil || len(resp.Update.Update) == 0 {
+		log.Debugf("handleSubscribeResponseUpdate empty update or updates; Update: %v, InnerUpdates: %v", resp.Update, resp.Update.Update)
+		return
+	}
+
 	for _, update := range resp.Update.Update {
 		pathString, err := ygot.PathToString(update.Path)
 		if err != nil {
diff --git a/dev_env_data/clab/gosdn_slim.clab.yaml b/dev_env_data/clab/gosdn_slim.clab.yaml
index f4d76846cddf22b054f8cee3c4e39d70589cb7ea..b3ef8eb088890c2e63d349c11d4d7de5f60a5e65 100644
--- a/dev_env_data/clab/gosdn_slim.clab.yaml
+++ b/dev_env_data/clab/gosdn_slim.clab.yaml
@@ -13,6 +13,13 @@ topology:
       image: plugin-registry
       mgmt-ipv4: 172.100.0.16
 
+    react-ui:
+      kind: linux
+      image: react-ui
+      ports:
+        - 127.0.0.1:8088:80
+      mgmt-ipv4: 172.100.0.6
+
     gosdn:
       kind: linux
       image: gosdn
diff --git a/dev_env_data/docker-compose/basic_docker-compose.yml b/dev_env_data/docker-compose/basic_docker-compose.yml
index 4e7664161a7a65487281e203f52d866271815d7c..2544e7b0f3728d57d64b3138dd7f5d6f74f7f4cc 100644
--- a/dev_env_data/docker-compose/basic_docker-compose.yml
+++ b/dev_env_data/docker-compose/basic_docker-compose.yml
@@ -75,5 +75,10 @@ services:
     command:
       start --cert /etc/gnmi-target/ssl/certs/gnmi-target-selfsigned.crt --key /etc/gnmi-target/ssl/private/gnmi-target-selfsigned.key --ca_file /etc/gnmi-target/ssl/ca.crt
 
+  react-ui:
+    image: react-ui
+    ports:
+      - 127.0.0.1:8088:80
+
 volumes:
     mongo-db-basic:
diff --git a/go.mod b/go.mod
index 2879fe71145322dbb2465f3d4479af08aec81257..f482650864f050c1a0a4805656c6778dbff70cc4 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,14 @@ module code.fbi.h-da.de/danet/gosdn
 go 1.23
 
 require (
-	github.com/aristanetworks/goarista v0.0.0-20241115153057-bd75d7f26a44
+	github.com/aristanetworks/goarista v0.0.0-20250108234106-1f88a86e2265
 	github.com/c-bata/go-prompt v0.2.6
 	github.com/docker/docker v24.0.9+incompatible
 	github.com/google/go-cmp v0.6.0
 	github.com/google/uuid v1.6.0
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1
 	github.com/mitchellh/go-homedir v1.1.0
-	github.com/openconfig/gnmi v0.11.0
+	github.com/openconfig/gnmi v0.12.0
 	github.com/openconfig/goyang v1.6.0
 	github.com/openconfig/ygot v0.29.20
 	github.com/prometheus/client_golang v1.20.5
@@ -22,10 +22,10 @@ require (
 	github.com/spf13/viper v1.19.0
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/stretchr/testify v1.10.0
-	go.mongodb.org/mongo-driver v1.17.1
+	go.mongodb.org/mongo-driver/v2 v2.0.0
 	golang.org/x/sync v0.10.0
-	google.golang.org/grpc v1.68.1
-	google.golang.org/protobuf v1.35.2
+	google.golang.org/grpc v1.69.4
+	google.golang.org/protobuf v1.36.2
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -41,13 +41,13 @@ require (
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt v3.2.2+incompatible
-	github.com/golang/glog v1.2.3
+	github.com/golang/glog v1.2.4
 	github.com/golang/protobuf v1.5.4
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/gookit/color v1.5.4 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/klauspost/compress v1.17.9 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
@@ -64,7 +64,7 @@ require (
 	github.com/pkg/term v1.2.0-beta.2 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.57.0 // indirect
+	github.com/prometheus/common v0.61.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rabbitmq/amqp091-go v1.10.0
 	github.com/rivo/uniseg v0.4.4 // indirect
@@ -77,21 +77,22 @@ require (
 	github.com/xdg-go/stringprep v1.0.4 // indirect
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
-	golang.org/x/crypto v0.30.0
-	golang.org/x/net v0.31.0
-	golang.org/x/sys v0.28.0 // indirect
-	golang.org/x/term v0.27.0 // indirect
+	golang.org/x/crypto v0.32.0
+	golang.org/x/net v0.34.0
+	golang.org/x/sys v0.29.0 // indirect
+	golang.org/x/term v0.28.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
 )
 
 require (
-	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1
-	github.com/bufbuild/protovalidate-go v0.7.3
+	buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20241127180247-a33202765966.1
+	github.com/bufbuild/protovalidate-go v0.8.2
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/go-plugin v1.4.10
 	github.com/lesismal/nbio v1.5.12
-	google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a
+	go.mongodb.org/mongo-driver v1.17.2
+	google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422
 )
 
 require (
@@ -103,7 +104,7 @@ require (
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 	github.com/containerd/console v1.0.3 // indirect
 	github.com/fatih/color v1.15.0 // indirect
-	github.com/google/cel-go v0.22.0 // indirect
+	github.com/google/cel-go v0.22.1 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-hclog v1.5.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
@@ -122,7 +123,7 @@ require (
 	github.com/stoewer/go-strcase v1.3.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
-	golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
+	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
 	gotest.tools/v3 v3.5.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 880acc9d162c50002ebb01111d0ee6477af85d60..2fc1f0098212c87292e3a462639a30cf266a8709 100644
--- a/go.sum
+++ b/go.sum
@@ -16,6 +16,10 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-2024092016423
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20240920164238-5a7b106cbb87.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI=
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1 h1:jLd96rDDNJ+zIJxvV/L855VEtrjR0G4aePVDlCpf6kw=
 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.1-20241127180247-a33202765966.1 h1:v223wh/bhlSHSc0tU9PXRWXHhkw3UWMtth7TmYGfHAQ=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.1-20241127180247-a33202765966.1/go.mod h1:/zlFuuECgFgewxwW6qQKgvMJ07YZkWlVkcSxEhJprJw=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20241127180247-a33202765966.1 h1:BICM6du/XzvEgeorNo4xgohK3nMTmEPViGyd5t7xVqk=
+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.2-20241127180247-a33202765966.1/go.mod h1:JnMVLi3qrNYPODVpEKG7UjHLl/d2zR221e66YCSmP2Q=
 cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
 cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@@ -56,6 +60,10 @@ github.com/aristanetworks/goarista v0.0.0-20241101122619-a6d58bf1ed81 h1:CpeoPCo
 github.com/aristanetworks/goarista v0.0.0-20241101122619-a6d58bf1ed81/go.mod h1:C+YeQrhbMvCPh5wG6iqGiCD/zcITTpt4YQ1v4K0g5Vc=
 github.com/aristanetworks/goarista v0.0.0-20241115153057-bd75d7f26a44 h1:vb3HPPa1CegMZY90JF8mDyxXiV+qJJuSWwMhBZCcsws=
 github.com/aristanetworks/goarista v0.0.0-20241115153057-bd75d7f26a44/go.mod h1:C+YeQrhbMvCPh5wG6iqGiCD/zcITTpt4YQ1v4K0g5Vc=
+github.com/aristanetworks/goarista v0.0.0-20250108214730-362a04c9d029 h1:bvw2TILeXtuYfZ9rip/4DY933UuIvCwtvJmwvz978ac=
+github.com/aristanetworks/goarista v0.0.0-20250108214730-362a04c9d029/go.mod h1:C+YeQrhbMvCPh5wG6iqGiCD/zcITTpt4YQ1v4K0g5Vc=
+github.com/aristanetworks/goarista v0.0.0-20250108234106-1f88a86e2265 h1:NPQhasGGtAIxtDG4KQTcQviV9T6a98kbKSO0VKFRS+E=
+github.com/aristanetworks/goarista v0.0.0-20250108234106-1f88a86e2265/go.mod h1:1xldiSdHhqa1XIr6EPNnSBfwZEAMZwwJIiEtMS8yzkU=
 github.com/aristanetworks/gomap v0.0.0-20240724180630-b4cffb90720f h1:3GwV1IeLp0PwWcnbc9ZihE3osvexJf3PMjWSCGjtIqc=
 github.com/aristanetworks/gomap v0.0.0-20240724180630-b4cffb90720f/go.mod h1:bNzH6HFWav8D/ws3QlkjLpf9ZOdsUTDx+qJikWCcGRc=
 github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
@@ -71,6 +79,8 @@ github.com/bufbuild/protovalidate-go v0.7.2 h1:UuvKyZHl5p7u3ztEjtRtqtDxOjRKX5VUO
 github.com/bufbuild/protovalidate-go v0.7.2/go.mod h1:PHV5pFuWlRzdDW02/cmVyNzdiQ+RNNwo7idGxdzS7o4=
 github.com/bufbuild/protovalidate-go v0.7.3 h1:kKnoSueygR3xxppvuBpm9SEwIsP359MMRfMBGmRByPg=
 github.com/bufbuild/protovalidate-go v0.7.3/go.mod h1:CFv34wMqiBzAHdQ4q/tWYi9ILFYKuaC3/4zh6eqdUck=
+github.com/bufbuild/protovalidate-go v0.8.2 h1:sgzXHkHYP6HnAsL2Rd3I1JxkYUyEQUv9awU1PduMxbM=
+github.com/bufbuild/protovalidate-go v0.8.2/go.mod h1:K6w8iPNAXBoIivVueSELbUeUl+MmeTQfCDSug85pn3M=
 github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI=
 github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY=
 github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@@ -124,6 +134,8 @@ github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
 github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/glog v1.2.3 h1:oDTdz9f5VGVVNGu/Q7UXKWYsD0873HXLHdJUNBsSEKM=
 github.com/golang/glog v1.2.3/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
+github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -146,6 +158,8 @@ github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
 github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
 github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g=
 github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
+github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40=
+github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -169,6 +183,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -190,6 +206,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
 github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
@@ -262,6 +280,8 @@ github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DV
 github.com/openconfig/gnmi v0.10.0/go.mod h1:Y9os75GmSkhHw2wX8sMsxfI7qRGAEcDh8NTa5a8vj6E=
 github.com/openconfig/gnmi v0.11.0 h1:H7pLIb/o3xObu3+x0Fv9DCK7TH3FUh7mNwbYe+34hFw=
 github.com/openconfig/gnmi v0.11.0/go.mod h1:9oJSQPPCpNvfMRj8e4ZoLVAw4wL8HyxXbiDlyuexCGU=
+github.com/openconfig/gnmi v0.12.0 h1:aPkmcX9pdcz6QqsBsXXg5UQooqhnmlHD3JtdtvtzmaU=
+github.com/openconfig/gnmi v0.12.0/go.mod h1:5a/cIOZevJLfJgd1qWkgYROE8xfgEbaSJXpdD8xk/LQ=
 github.com/openconfig/goyang v0.0.0-20200115183954-d0a48929f0ea/go.mod h1:dhXaV0JgHJzdrHi2l+w0fZrwArtXL7jEFoiqLEdmkvU=
 github.com/openconfig/goyang v1.6.0 h1:JjnPbLY1/y28VyTO67LsEV0TaLWNiZyDcsppGq4F4is=
 github.com/openconfig/goyang v1.6.0/go.mod h1:sdNZi/wdTZyLNBNfgLzmmbi7kISm7FskMDKKzMY+x1M=
@@ -296,6 +316,8 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.57.0 h1:Ro/rKjwdq9mZn1K5QPctzh+MA4Lp0BuYk5ZZEVhoNcY=
 github.com/prometheus/common v0.57.0/go.mod h1:7uRPFSUTbfZWsJ7MHY56sqt7hLQu3bxXHDnNhl8E9qI=
+github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
+github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/protocolbuffers/txtpbfmt v0.0.0-20220608084003-fc78c767cd6a/go.mod h1:KjY0wibdYKc4DYkerHSbguaf3JeIPGhNJBp2BNiFH78=
@@ -390,6 +412,9 @@ go.mongodb.org/mongo-driver v1.17.0 h1:Hp4q2MCjvY19ViwimTs00wHi7G4yzxh4/2+nTx8r4
 go.mongodb.org/mongo-driver v1.17.0/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
 go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
 go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
+go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
+go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
+go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -411,9 +436,15 @@ golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
 golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
 golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
 golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
 golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
+golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -446,6 +477,10 @@ golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
 golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
 golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -501,6 +536,8 @@ golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
 golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
 golang.org/x/sys v0.28.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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -514,6 +551,8 @@ golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
 golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
 golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
 golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -575,6 +614,10 @@ google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
 google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
 google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA=
+google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
+google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
@@ -595,6 +638,12 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241113202542-65e8d215514f/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
@@ -613,6 +662,10 @@ google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
 google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
 google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
 google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
+google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
+google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
+google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
+google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -633,6 +686,10 @@ google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFyt
 google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
 google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
+google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
+google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/makefiles/container/Makefile b/makefiles/container/Makefile
index c9c21b9728af4606335ea425a187a55dd09f5ad1..6e3b97242bdd968d9e4236440721c9bea30bc805 100644
--- a/makefiles/container/Makefile
+++ b/makefiles/container/Makefile
@@ -25,3 +25,6 @@ containerize-ws-events-app:
 
 containerize-inventory-manager:
 	docker buildx build --rm -t venv-manager --load -f applications/inventory-manager/inventory-manager.Dockerfile .
+
+containerize-react-ui:
+	docker buildx build --rm --load -t react-ui -f react-ui/docker/webserver/Dockerfile .
diff --git a/plugin-registry/plugin-registry.Dockerfile b/plugin-registry/plugin-registry.Dockerfile
index 05d3aad83fd593051616bfc5345367d961f8adeb..292a134fd5707a0508ad136b8a01d8e2f3d0890d 100644
--- a/plugin-registry/plugin-registry.Dockerfile
+++ b/plugin-registry/plugin-registry.Dockerfile
@@ -4,8 +4,9 @@ ARG GITLAB_PROXY
 
 FROM ${GITLAB_PROXY}golang:$GOLANG_VERSION-bookworm as builder
 WORKDIR /plugin-registry/
-RUN apt-get update
-RUN apt-get -y install --no-install-recommends zip
+RUN apt-get update && \
+    apt-get -y install --no-install-recommends zip && \
+    rm -rf /var/lib/apt/lists/*
 COPY . .
 RUN --mount=type=cache,target=/root/go/pkg/mod \
     --mount=type=cache,target=/root/.cache/go-build \
@@ -21,4 +22,4 @@ WORKDIR /app/
 COPY --from=builder /plugin-registry/artifacts/plugin-registry .
 COPY --from=builder /plugin-registry/plugin-registry/plugins ./plugins
 COPY --from=builder /plugin-registry/dev_env_data/plugin-registry/plugin-store.json ./plugin-store.json
-ENTRYPOINT ["./plugin-registry", "-socket", "55057"]
+ENTRYPOINT ["./plugin-registry", "-socket", "55057"]
\ No newline at end of file
diff --git a/react-ui/.dockerignore b/react-ui/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..5ecec04724f8685ae46c6f5ddf66a4c92844754f
--- /dev/null
+++ b/react-ui/.dockerignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+vite/
+tmp/
+.vscode/
\ No newline at end of file
diff --git a/react-ui/assets/logo.svg b/react-ui/assets/logo.svg
deleted file mode 100755
index b7f71bd907d8ab0aa829f03519d4d6f06cdadfec..0000000000000000000000000000000000000000
--- a/react-ui/assets/logo.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<!-- Created with Vectornator (http://vectornator.io/) -->
-<svg height="100%" stroke-miterlimit="10" style="fill-rule:nonzero;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;" version="1.1" viewBox="0 0 113.4 212.625" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:vectornator="http://vectornator.io" xmlns:xlink="http://www.w3.org/1999/xlink">
-<defs>
-<radialGradient cx="170.235" cy="146.046" gradientTransform="matrix(1.00001 -8.65109e-05 8.65119e-05 1 -115.465 -116.986)" gradientUnits="userSpaceOnUse" id="RadialGradient" r="217.591">
-<stop offset="0" stop-color="#c456f7"/>
-<stop offset="1" stop-color="#34054a"/>
-</radialGradient>
-<filter color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse" height="209.692" id="Filter" width="111.957" x="0.722269" y="2.46642">
-<feDropShadow dx="-4.37114e-08" dy="1" flood-color="#050505" flood-opacity="1" in="SourceGraphic" result="Shadow" stdDeviation="1"/>
-</filter>
-</defs>
-<g id="Layer-1" vectornator:layerName="Layer 1">
-<path d="M35.1413 3.47016L35.1416 6.93891L27.1415 6.93961L27.1464 63.3771C21.6096 65.0011 16.4869 67.0445 12.2717 69.6596C-9.35807 83.0789 12.5182 123.232 12.5264 123.222C12.5341 123.213 12.5498 123.2 12.5576 123.191C13.1518 124.609 14.5144 125.761 16.5579 126.534C18.7737 127.372 21.8204 127.855 26.8082 128.408C25.0644 132.318 24.4639 137.732 25.3409 144.846C26.0816 150.854 28.2327 155.309 32.5924 161.533C32.9615 162.06 34.1761 163.796 34.3427 164.033C35.013 164.984 35.5416 165.705 35.9991 166.408C38.1084 169.647 39.1319 172.149 39.3124 174.814C39.3701 175.665 39.3355 180.513 39.251 187.751C39.2362 189.024 39.0999 200.614 39.0959 200.907C37.7122 201.653 36.7523 203.069 36.7525 204.751C36.7527 207.19 38.7205 209.157 41.1591 209.157C43.5978 209.157 45.5653 207.189 45.5651 204.751C45.5649 203.019 44.5477 201.568 43.096 200.844C43.1023 200.341 43.2363 189.045 43.2511 187.782C43.3388 180.268 43.3816 175.583 43.3124 174.563C43.0775 171.096 41.7912 168.012 39.3427 164.251C38.854 163.5 38.3197 162.708 37.6237 161.72C37.4507 161.474 36.2342 159.766 35.8735 159.251C31.8564 153.517 29.9529 149.563 29.3096 144.346C28.2621 135.848 29.4895 130.5 32.027 127.533C32.6413 126.815 33.2789 126.306 33.9019 125.97C34.2334 125.792 34.4442 125.7 34.4956 125.689C35.5735 125.448 36.2365 124.392 35.9954 123.314C35.7543 122.236 34.6982 121.542 33.6202 121.783C33.2264 121.871 32.6655 122.078 31.9953 122.439C31.0669 122.939 30.1635 123.734 29.3079 124.658C23.0392 124.019 20.0922 123.577 17.9951 122.784C16.1482 122.086 15.9773 121.63 16.7137 120.347C25.511 115.923 43.9287 113.157 55.0885 113.156C63.8081 113.155 79.7952 116.873 90.4333 119.622C93.9977 120.542 94.7294 120.755 96.621 121.277C94.2649 123.034 89.4439 124.738 83.3087 125.247C83.0835 125.266 82.9434 125.443 82.7462 125.529C82.6074 125.348 82.5171 125.106 82.3711 124.935C81.4271 123.831 80.3863 122.999 79.3396 122.435C78.6693 122.074 78.1085 121.867 77.7145 121.779C76.6365 121.538 75.5805 122.233 75.3396 123.31C75.0987 124.388 75.7619 125.444 76.8399 125.685C76.8913 125.697 77.1021 125.788 77.4337 125.967C78.0567 126.302 78.6942 126.81 79.3088 127.529C81.8468 130.496 83.0751 135.844 82.029 144.341C81.3867 149.559 79.4839 153.513 75.4678 159.248C75.1071 159.763 73.8909 161.471 73.7179 161.717C73.0221 162.705 72.488 163.498 71.9994 164.248C69.5516 168.009 68.2658 171.094 68.0315 174.561C67.9625 175.581 68.0061 180.266 68.0951 187.78C68.1102 189.055 68.2793 200.673 68.2838 200.967C66.9421 201.725 66.0027 203.098 66.0028 204.749C66.0031 207.187 67.9709 209.155 70.4095 209.155C72.8481 209.154 74.8156 207.187 74.8154 204.748C74.8153 202.975 73.7598 201.482 72.2526 200.779C72.2461 200.277 72.1101 189.009 72.0952 187.748C72.0095 180.511 71.974 175.662 72.0316 174.811C72.2116 172.146 73.2659 169.644 75.3746 166.404C75.8321 165.701 76.3292 164.981 76.9994 164.029C77.1659 163.793 78.3803 162.056 78.7492 161.529C83.1079 155.305 85.2582 150.849 85.9979 144.841C86.8306 138.077 86.2654 132.917 84.7153 129.06C93.073 128.178 99.944 125.471 101.777 121.527C105.258 116.592 120.674 81.4184 100.46 69.1833C96.4145 66.7344 91.6503 64.7196 86.3971 63.1845L86.3922 6.93448L78.3609 6.93517L78.3606 3.46642L35.1413 3.47016ZM31.1419 10.9393L39.1107 10.9386L39.3309 27.9386C39.3406 28.6841 39.9605 29.2606 40.7061 29.2509C41.4516 29.2413 42.0593 28.6214 42.0497 27.8758L41.7983 10.9383L44.517 10.9381L44.7998 27.9381C44.8108 28.6836 45.3981 29.2615 46.1436 29.2505C46.8892 29.2394 47.4983 28.6209 47.4873 27.8753L47.2358 10.9379L49.9546 10.9376L50.2061 27.9376C50.2169 28.6831 50.8357 29.2608 51.5812 29.25C52.3267 29.2392 52.9044 28.6203 52.8936 27.8749L52.6421 10.9374L55.3609 10.9372L55.6124 27.9371C55.623 28.6827 56.242 29.2601 56.9875 29.2495C57.733 29.2389 58.3418 28.6199 58.3312 27.8744L58.0797 10.9369L60.8297 10.9367L61.05 27.9367C61.0546 28.3094 61.2147 28.6381 61.4563 28.8741C61.6979 29.1102 62.0211 29.2537 62.3938 29.2491C63.1394 29.2398 63.7468 28.6194 63.7375 27.8739L63.5173 10.9365L66.2986 10.9362L66.4875 27.905C66.4957 28.6505 67.1171 29.2568 67.8627 29.2486C68.6082 29.2404 69.1834 28.6503 69.1751 27.9047L68.9861 10.936L71.7674 10.9357L71.9564 27.9357C71.9605 28.3085 72.1132 28.6372 72.3627 28.8732C72.6122 29.1091 72.9587 29.2523 73.3315 29.2481C74.077 29.2398 74.6833 28.6498 74.6751 27.9042L74.4862 10.9355L82.3925 10.9348L82.3969 62.2473C78.1953 61.2363 73.8195 60.4151 69.2716 59.936L69.0841 59.936L69.0846 65.9672L62.3352 73.3116L62.211 82.5928L62.212 94.7803L56.5244 94.7808L50.3056 94.7814L50.2733 82.9376L50.1163 73.3126L43.1468 65.8132L43.1463 60.0945L31.1463 62.3455L31.1419 10.9393ZM46.0197 41.188L45.9904 64.3755L52.9599 71.8749L53.1492 91.9374L59.368 91.9368L59.5225 71.8743L66.2407 64.5612L66.2387 41.3425L46.0197 41.188ZM45.5578 121.282C45.0484 121.232 44.5158 121.369 44.0891 121.719C43.2356 122.421 43.1382 123.71 43.8394 124.563C43.8712 124.602 43.9181 124.67 44.0269 124.813C44.2163 125.062 44.4521 125.354 44.6832 125.688C45.3494 126.65 45.998 127.755 46.621 128.938C48.9603 133.379 50.0248 137.836 49.1222 141.875C48.4351 144.95 46.6061 147.711 43.4353 150.126C43.1568 150.338 42.1644 150.942 40.7792 151.751C40.6905 151.803 37.4839 153.662 37.4356 153.689C36.4728 154.23 36.1131 155.445 36.6545 156.408C37.1959 157.37 38.4106 157.699 39.3734 157.157C39.4251 157.128 42.7199 155.272 42.8108 155.22C44.3781 154.304 45.4057 153.669 45.8731 153.313C49.7829 150.336 52.1324 146.76 53.0285 142.75C54.1746 137.622 52.9238 132.266 50.1834 127.063C49.4811 125.729 48.7181 124.526 47.9643 123.438C47.5056 122.775 47.1244 122.271 46.9017 122C46.5511 121.574 46.0673 121.332 45.5578 121.282ZM65.7769 121.28C65.2675 121.33 64.7837 121.572 64.4331 121.999C64.2105 122.27 63.8293 122.774 63.3708 123.436C62.6172 124.525 61.8858 125.728 61.1835 127.062C58.444 132.265 57.163 137.621 58.3099 142.749C59.2067 146.759 61.5569 150.335 65.4671 153.311C65.9346 153.667 66.9935 154.302 68.5611 155.217C68.6519 155.27 71.9158 157.125 71.9675 157.154C72.9304 157.696 74.145 157.367 74.6862 156.404C75.2275 155.441 74.8676 154.227 73.9048 153.686C73.8565 153.658 70.6495 151.8 70.5608 151.748C69.1754 150.939 68.1829 150.336 67.9044 150.124C64.7332 147.71 62.9036 144.948 62.2161 141.874C61.3128 137.835 62.3766 133.378 64.715 128.936C65.3378 127.753 65.9862 126.649 66.6522 125.686C66.8833 125.352 67.1191 125.06 67.3084 124.811C67.4172 124.668 67.4953 124.6 67.5272 124.561C68.2282 123.708 68.0992 122.418 67.2457 121.717C66.8189 121.367 66.2862 121.23 65.7769 121.28ZM28.3735 160.064C27.8641 160.114 27.3492 160.357 26.9985 160.783C24.0604 164.358 22.5881 168.723 22.5621 174.253C22.5614 174.404 22.6311 200.839 22.627 200.877C21.1966 201.608 20.1896 203.036 20.1898 204.753C20.19 207.191 22.1578 209.159 24.5964 209.159C27.0351 209.158 29.0026 207.191 29.0024 204.752C29.0022 203.061 28.0235 201.65 26.627 200.908C26.6562 200.425 26.6538 197.055 26.6259 187.658C26.6229 186.662 26.5615 174.385 26.5622 174.252C26.584 169.618 27.7563 166.157 30.0925 163.314C30.7939 162.461 30.6644 161.203 29.811 160.502C29.3844 160.151 28.8829 160.015 28.3735 160.064ZM82.9679 160.06C82.4585 160.01 81.9571 160.147 81.5304 160.497C80.6772 161.199 80.5479 162.457 81.2494 163.31C83.5862 166.152 84.7591 169.613 84.7817 174.247C84.7823 174.378 84.7241 199.67 84.7527 200.841C83.3011 201.564 82.2529 203.015 82.253 204.747C82.2533 207.186 84.2524 209.153 86.691 209.153C89.1295 209.153 91.0971 207.185 91.0969 204.747C91.0967 203.049 90.1265 201.611 88.7215 200.872C88.7173 200.833 88.7318 194.753 88.7516 187.684C88.7544 186.688 88.7824 174.398 88.7817 174.247C88.7548 168.717 87.2817 164.353 84.343 160.778C83.9922 160.352 83.4774 160.109 82.9679 160.06Z" fill="url(#RadialGradient)" fill-rule="nonzero" filter="url(#Filter)" stroke="none" vectornator:shadowAngle="1.5708" vectornator:shadowColor="#050505" vectornator:shadowOffset="1" vectornator:shadowOpacity="1" vectornator:shadowRadius="2"/>
-</g>
-</svg>
diff --git a/react-ui/docker/webserver/Dockerfile b/react-ui/docker/webserver/Dockerfile
index f4644ee1036b55d30673e0290d3a89aec8a68c60..9c4f3f6765bed330e062267d329db9f2f5e04ce9 100644
--- a/react-ui/docker/webserver/Dockerfile
+++ b/react-ui/docker/webserver/Dockerfile
@@ -1,4 +1,17 @@
-FROM nginx:alpine3.20
+FROM node:alpine3.20 as builder
+
+COPY  ./api/openapiv2/gosdn_northbound.swagger.json /app/api/openapiv2/gosdn_northbound.swagger.json
+COPY ./react-ui /app/react-ui
+
+RUN cd /app/react-ui && \
+    rm -rf node_modules && \
+    rm yarn.lock && \
+    yarn install --production && \
+    yarn build
 
-COPY dist /usr/share/nginx/html
-COPY docker/webserver/nginx.conf /etc/nginx/nginx.conf 
\ No newline at end of file
+
+# webserver
+FROM nginx:alpine3.20
+COPY --from=builder /app/react-ui/dist /usr/share/nginx/html
+COPY --from=builder /app/react-ui/docker/webserver/nginx.conf /etc/nginx/nginx.conf 
+EXPOSE 80
\ No newline at end of file
diff --git a/react-ui/docker/webserver/nginx.conf b/react-ui/docker/webserver/nginx.conf
index 87066f6ab89b1a840816f93fedc989e0d0736bfb..b9028bec32ba630f8277d054dcbfbac37d707297 100644
--- a/react-ui/docker/webserver/nginx.conf
+++ b/react-ui/docker/webserver/nginx.conf
@@ -1,52 +1,82 @@
-
-#user  nobody;
 worker_processes  1;
 
-#error_log  logs/error.log;
-#error_log  logs/error.log  notice;
-#error_log  logs/error.log  info;
-
-#pid        logs/nginx.pid;
-
-
 events {
     worker_connections  1024;
+    multi_accept on;
+    use epoll;
 }
 
 http {
     include       mime.types;
     default_type  application/octet-stream;
 
-    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
-    #                  '$status $body_bytes_sent "$http_referer" '
-    #                  '"$http_user_agent" "$http_x_forwarded_for"';
-
-    #access_log  logs/access.log  main;
-
-    sendfile        on;
-    #tcp_nopush     on;
-
-    #keepalive_timeout  0;
-    keepalive_timeout  65;
+    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+                    '$status $body_bytes_sent "$http_referer" '
+                    '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log /var/log/nginx/access.log main buffer=16k;
+
+    sendfile on;
+    tcp_nopush on;
+    tcp_nodelay on;
+    keepalive_timeout 65;
+    types_hash_max_size 2048;
+    server_tokens off;
+
+    # Buffer size settings
+    client_body_buffer_size 10K;
+    client_header_buffer_size 8k;
+    client_max_body_size 8m;
+    large_client_header_buffers 4 8k;
+
+    # File descriptor cache
+    open_file_cache max=2000 inactive=20s;
+    open_file_cache_valid 60s;
+    open_file_cache_min_uses 5;
+    open_file_cache_errors off;
+
+    # Compression settings
+    gzip on;
+    gzip_comp_level 6;
+    gzip_min_length 256;
+    gzip_proxied any;
+    gzip_vary on;
+    gzip_types
+        application/javascript
+        application/json
+        application/x-javascript
+        application/xml
+        text/css
+        text/javascript
+        text/plain
+        text/xml
+        text/html
+        application/x-font-ttf
+        font/opentype
+        application/vnd.ms-fontobject
+        image/svg+xml;
 
     resolver 127.0.0.11 ipv6=off;
 
-    #gzip  on;
-
     server {
         listen       80;
         server_name  localhost;
 
-        #charset koi8-r;
-
-        #access_log  logs/host.access.log  main;
-
-
         location ^~ /api/ {
             proxy_set_header Host $host;
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
             proxy_set_header X-Forwarded-Proto $scheme;
+
+            # Proxy timeouts
+            proxy_connect_timeout 60s;
+            proxy_send_timeout 60s;
+            proxy_read_timeout 60s;
+            
+            # Proxy buffering
+            proxy_buffering on;
+            proxy_buffer_size 4k;
+            proxy_buffers 8 16k;
             
             # CORS headers
             add_header 'Access-Control-Allow-Origin' '*' always;
@@ -75,59 +105,23 @@ http {
             try_files $uri $uri/ /index.html;
         }
 
-        location ~* \.(js|css|jpg|png|svg|woff|woff2|ttf|otf|eot|ico)$ {
+
+        # Static asset handling with improved caching
+        location ~* \.(js|css|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|otf|eot)$ {
             root /usr/share/nginx/html;
-            add_header 'Access-Control-Allow-Origin' '*' always;
             expires 30d;
-            add_header Cache-Control "public";
+            add_header Cache-Control "public, no-transform";
+            add_header 'Access-Control-Allow-Origin' '*' always;
+            
+            # Enable compression for these files
+            gzip_static on;  # Serve pre-compressed files if available
+            
+            # Disable access logs for static files
+            access_log off;
         }
-
-        # #error_page  404              /404.html;
-
-        # # redirect server error pages to the static page /50x.html
-        # #
-        # error_page   500 502 503 504  /50x.html;
-        # location = /50x.html {
-        #     root   html;
-        # }
-
-        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
-        #
-        #location ~ \.php$ {
-        #    proxy_pass   http://127.0.0.1;
-        #}
-
-        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
-        #
-        #location ~ \.php$ {
-        #    root           html;
-        #    fastcgi_pass   127.0.0.1:9000;
-        #    fastcgi_index  index.php;
-        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
-        #    include        fastcgi_params;
-        #}
-
-        # deny access to .htaccess files, if Apache's document root
-        # concurs with nginx's one
-        #
-        #location ~ /\.ht {
-        #    deny  all;
-        #}
     }
 
 
-    # another virtual host using mix of IP-, name-, and port-based configuration
-    #
-    #server {
-    #    listen       8000;
-    #    listen       somename:8080;
-    #    server_name  somename  alias  another.alias;
-
-    #    location / {
-    #        root   html;
-    #        index  index.html index.htm;
-    #    }
-    #}
 
 
     # HTTPS server
@@ -150,5 +144,4 @@ http {
     #        index  index.html index.htm;
     #    }
     #}
-
 }
\ No newline at end of file
diff --git a/react-ui/index.html b/react-ui/index.html
index 91d50a11c836668b61e6e60fee7dabe940186c0f..3dc374a596e8a0a21a920783305d30984626c95e 100755
--- a/react-ui/index.html
+++ b/react-ui/index.html
@@ -5,13 +5,14 @@
     <link rel="icon" href="favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="theme-color" content="#000000" />
+    <meta name="author" content="Matthias Feyll" />
     <meta
       name="description"
-      content="Web site created using create-react-app"
+      content="goSDN web ui"
     />
     <link rel="apple-touch-icon" href="logo.png" />
     <link rel="manifest" href="manifest.json" />
-    <title>goSDN</title>
+    <title>goSDN - ui</title>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
diff --git a/react-ui/package.json b/react-ui/package.json
index 709c5bfcb623cd76f6b0e5264378ad3f909a58a7..1f742d78da361ba160c390cd6296d007c4539b42 100755
--- a/react-ui/package.json
+++ b/react-ui/package.json
@@ -12,8 +12,12 @@
         "@fortawesome/free-regular-svg-icons": "^6.6.0",
         "@fortawesome/free-solid-svg-icons": "^6.6.0",
         "@fortawesome/react-fontawesome": "^0.2.2",
+        "@fullhuman/postcss-purgecss": "^7.0.2",
         "@reduxjs/toolkit": "^2.2.4",
+        "@types/react-grid-layout": "^1.3.5",
+        "@vitejs/plugin-react": "^4.2.1",
         "bootstrap": "^5.3.3",
+        "crypto-js": "^4.2.0",
         "dompurify": "^3.2.3",
         "i18next": "^24.0.5",
         "jwt-decode": "^4.0.0",
@@ -21,14 +25,43 @@
         "react-bootstrap": "^2.10.2",
         "react-dom": "^18.3.1",
         "react-error-boundary": "^4.1.2",
+        "react-grid-layout": "^1.5.0",
         "react-i18next": "^15.0.0",
         "react-redux": "^9.1.2",
         "react-router-dom": "^6.23.1",
-        "react-scripts": "5.0.1",
         "react-toastify": "^10.0.5",
         "redux": "^5.0.1",
         "redux-observable": "^3.0.0-rc.2",
         "redux-persist": "^6.0.0",
+        "sass": "1.82.0",
+        "sass-embedded": "^1.80.6",
+        "vite": "^6.0.3"
+    },
+    "devDependencies": {
+        "@babel/runtime": "^7.21.5",
+        "@rtk-query/codegen-openapi": "^2.0.0",
+        "@testing-library/jest-dom": "^6.4.8",
+        "@testing-library/react": "^16.0.0",
+        "@testing-library/user-event": "^14.5.2",
+        "@types/react": "^18.2.66",
+        "@types/react-dom": "^18.2.22",
+        "@typescript-eslint/eslint-plugin": "^8.0.1",
+        "@typescript-eslint/parser": "^8.0.1",
+        "eslint": "^9.9.0",
+        "eslint-config-airbnb-typescript": "^18.0.0",
+        "eslint-config-prettier": "^9.1.0",
+        "eslint-plugin-import": "^2.27.5",
+        "eslint-plugin-jsx-a11y": "^6.7.1",
+        "eslint-plugin-prettier": "^5.2.1",
+        "eslint-plugin-react": "^7.32.2",
+        "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+        "eslint-plugin-react-refresh": "^0.4.9",
+        "globals": "^15.9.0",
+        "prettier": "^3.3.3",
+        "react-scripts": "5.0.1",
+        "typescript": "^5.5.3",
+        "typescript-eslint": "^8.0.1",
+        "vite-bundle-visualizer": "^1.2.1",
         "web-vitals": "^4.2.2"
     },
     "scripts": {
@@ -37,7 +70,9 @@
         "build::api": "npx @rtk-query/codegen-openapi ./scripts/openapi-config.json",
         "build": "yarn build::api && yarn build::frontend",
         "lint": "eslint src",
-        "lint::fix": "eslint src --fix"
+        "lint::fix": "eslint src --fix",
+        "dev": "./scripts/dev.sh",
+        "postbuild": "purgecss --css dist/assets/*.css --content dist/index.html dist/assets/*.js --output dist/purged"
     },
     "eslintConfig": {
         "extends": [
@@ -55,34 +90,5 @@
             "last 1 firefox version",
             "last 1 safari version"
         ]
-    },
-    "devDependencies": {
-        "@babel/runtime": "^7.21.5",
-        "@rtk-query/codegen-openapi": "^2.0.0",
-        "@testing-library/jest-dom": "^6.4.8",
-        "@testing-library/react": "^16.0.0",
-        "@testing-library/user-event": "^14.5.2",
-        "@types/react": "^18.2.66",
-        "@types/react-dom": "^18.2.22",
-        "@typescript-eslint/eslint-plugin": "^8.0.1",
-        "@typescript-eslint/parser": "^8.0.1",
-        "@vitejs/plugin-react": "^4.2.1",
-        "eslint": "^9.9.0",
-        "eslint-config-airbnb-typescript": "^18.0.0",
-        "eslint-config-prettier": "^9.1.0",
-        "eslint-plugin-import": "^2.27.5",
-        "eslint-plugin-jsx-a11y": "^6.7.1",
-        "eslint-plugin-prettier": "^5.2.1",
-        "eslint-plugin-react": "^7.32.2",
-        "eslint-plugin-react-hooks": "^5.1.0-rc.0",
-        "eslint-plugin-react-refresh": "^0.4.9",
-        "globals": "^15.9.0",
-        "prettier": "^3.3.3",
-        "sass": "1.82.0",
-        "sass-embedded": "^1.80.6",
-        "typescript": "^5.5.3",
-        "typescript-eslint": "^8.0.1",
-        "vite": "^6.0.3",
-        "vite-bundle-visualizer": "^1.2.1"
     }
-}
\ No newline at end of file
+}
diff --git a/react-ui/public/favicon.ico b/react-ui/public/favicon.ico
old mode 100755
new mode 100644
index a11777cc471a4344702741ab1c8a588998b1311a..ae6dcf05241b89f446e88d15b81ffd69202fa83a
Binary files a/react-ui/public/favicon.ico and b/react-ui/public/favicon.ico differ
diff --git a/react-ui/public/fonts/inter-webfont.woff b/react-ui/public/fonts/inter-webfont.woff
new file mode 100644
index 0000000000000000000000000000000000000000..7eca6fe75d3ba56e14981e8f8e1c50d948bb8b56
Binary files /dev/null and b/react-ui/public/fonts/inter-webfont.woff differ
diff --git a/react-ui/public/fonts/inter-webfont.woff2 b/react-ui/public/fonts/inter-webfont.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..25ef870d53d5616ee3e92bbe3e85b5ed164fe4d2
Binary files /dev/null and b/react-ui/public/fonts/inter-webfont.woff2 differ
diff --git a/react-ui/scripts/build.sh b/react-ui/scripts/build.sh
index ad0f0cd0b20382110f1dbff59206f95c1d5a14f2..fb6cb2a21737c17e269f4dbc7fd1b2befdd5e6ae 100755
--- a/react-ui/scripts/build.sh
+++ b/react-ui/scripts/build.sh
@@ -7,7 +7,7 @@ docker run --rm \
     -w /app \
     -u $(id -u):$(id -g) \
     $IMAGE \
-    yarn install && yarn build
+    yarn && yarn build
 
 if [ $? -ne 0 ]; then
     echo "Error while building frontend app"
diff --git a/react-ui/scripts/dev.sh b/react-ui/scripts/dev.sh
new file mode 100755
index 0000000000000000000000000000000000000000..3a2d208858603433cf61c69ade5aa21d46f26377
--- /dev/null
+++ b/react-ui/scripts/dev.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env sh
+
+docker run \
+    -it \
+    --rm \
+    -v $(pwd):/app \
+    -w /app \
+    -p 127.0.0.1:3000:3000 \
+    --network gosdn-csbi-arista-base-net \
+    node:20-alpine3.20 \
+    npx vite
diff --git a/react-ui/src/components/devices/reducer/device.reducer.ts b/react-ui/src/components/devices/reducer/device.reducer.ts
index 8e4454bd285396b3aca4dce62bc7d1b1518fcd51..f211fe0249a5c423d3e90012fdc8cab7284ec6cc 100755
--- a/react-ui/src/components/devices/reducer/device.reducer.ts
+++ b/react-ui/src/components/devices/reducer/device.reducer.ts
@@ -1,107 +1,162 @@
 import {
-    api,
     NetworkelementFlattenedManagedNetworkElement,
     NetworkelementManagedNetworkElement,
-    PndPrincipalNetworkDomain,
-    PndServiceGetPndListApiArg,
+    PndPrincipalNetworkDomain
 } from '@api/api'
 import { DeviceViewTabValues } from '@component/devices/view/device.view.tabs'
-import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import { refreshUpdateTimer } from '@shared/reducer/routine.reducer'
+import { Category, CategoryType } from '@shared/types/category.type'
+import { REHYDRATE } from 'redux-persist'
 import { RootState } from 'src/stores'
 import '../routines/index'
 import { startListening } from '/src/stores/middleware/listener.middleware'
 
 export type Device = NetworkelementFlattenedManagedNetworkElement
 
-interface SelectedDeviceInterface {
+interface SelectedObject {
     device: Device
     mne: NetworkelementManagedNetworkElement | null
     json: JSON | null
 }
 
-type SelectedDeviceType = SelectedDeviceInterface | null
-
 export interface DeviceSliceState {
     devices: Device[]
     pnds: PndPrincipalNetworkDomain[]
 
     activeTab: DeviceViewTabValues
-    selectedDevice: SelectedDeviceType
+    selected: SelectedObject | null
 }
 
 const initialState: DeviceSliceState = {
     devices: [],
     pnds: [],
     activeTab: DeviceViewTabValues.METADATA,
-    selectedDevice: null,
+    selected: null,
+}
+
+interface SetSelectedDeviceType {
+    device: Device | null,
+    options?: {
+        bypassCheck: boolean
+    }
 }
 
 const deviceSlice = createSlice({
     name: 'device',
     initialState,
     reducers: {
-        setDevices: (state, action: PayloadAction<Device[]>) => {
-            state.devices = action.payload
+        setDevices: (state, action: PayloadAction<Device[] | undefined>) => {
+            state.devices = action.payload || []
         },
-        setPnds: (state, action: PayloadAction<PndPrincipalNetworkDomain[]>) => {
-            state.pnds = action.payload
+        setPnds: (state, action: PayloadAction<PndPrincipalNetworkDomain[] | undefined>) => {
+            state.pnds = action.payload || []
         },
         setActiveTab: (state, action: PayloadAction<DeviceViewTabValues>) => {
             state.activeTab = action.payload
         },
-        setSelectedDevice: (state, action: PayloadAction<Device | null>) => {
-            let selectedDevice: SelectedDeviceType = null
-            if (action.payload) {
-                selectedDevice = { device: action.payload, mne: null, json: null }
+        setSelectedDevice: {
+            reducer: (state, { payload, meta }: PayloadAction<SetSelectedDeviceType, string, { skipListener?: boolean }>) => {
+                /**
+                 * Do nothing if desired device is already selected
+                 * Bypass the check if the flag is set to true. We
+                 * use this bypass to trigger the listener functions
+                 * accordingly
+                 */
+                if (!payload?.options?.bypassCheck && state.selected?.device.id === payload.device?.id) {
+                    meta.skipListener = true
+                    return
+                }
+
+                if (!payload.device) {
+                    throw Error('Passed null device as parameter while bypassing the safety check')
+                }
+
+                let selectedObject: SelectedObject | null = null;
+                if (payload) {
+                    selectedObject = { device: payload.device, mne: null, json: null }
+                }
+
+                state.selected = selectedObject
+            },
+            prepare: (payload) => {
+                return {
+                    payload,
+                    meta: { skipListener: false }
+                }
             }
-
-            state.selectedDevice = selectedDevice
         },
         setSelectedMne: (state, action: PayloadAction<NetworkelementManagedNetworkElement>) => {
-            if (!state.selectedDevice) {
-                throw new Error('Selected Device is null where it shouldn´t be null')
+            if (!state.selected) {
+                throw new Error('Can not find corresponding device')
+            }
+
+            // safety check to prevent possible race conditions
+            if (state.selected.device.id !== action.payload.id) {
+                // TODO proper error handling by retry fetching the device object
+                throw new Error('Device and mne id does not match')
             }
 
-            state.selectedDevice.mne = action.payload
-            // TODO maybe don´t take the device of "selected device" instead search in the devices array for the proper device
-            // should not make a diffrence due to pointer but dunno
+            state.selected.mne = action.payload
         },
 
         setSelectedJson: (state, action: PayloadAction<JSON>) => {
-            if (!state.selectedDevice) {
+            if (!state.selected) {
                 throw new Error('Selected Device is null where it shouldn´t be null')
             }
 
-            state.selectedDevice.json = action.payload || null
+            state.selected.json = action.payload || null
         },
     },
 })
 
-export const { setDevices, setActiveTab, setSelectedDevice, setSelectedMne, setSelectedJson } =
+export const { setDevices, setActiveTab, setSelectedDevice, setSelectedMne, setSelectedJson, setPnds } =
     deviceSlice.actions
-const { setPnds } = deviceSlice.actions
 
 export default deviceSlice.reducer
 export const deviceReducerPath = deviceSlice.reducerPath
 
-export const fetchPnds = createAsyncThunk('device/fetchPnds', (_, thunkApi) => {
-    const payload: PndServiceGetPndListApiArg = {
-        timestamp: new Date().getTime().toString(),
-    }
+// add default selected device if no selected device is set
+startListening({
+    predicate: (action) => setDevices.match(action),
+    effect: async (action, listenerApi) => {
+        const { device: state } = listenerApi.getOriginalState() as RootState
+        if (state.selected) {
+            return
+        }
 
-    const subscription = thunkApi.dispatch(api.endpoints.pndServiceGetPndList.initiate(payload))
-    subscription.unwrap().then((response) => {
-        thunkApi.dispatch(setPnds(response.pnd))
-    })
+        // if there are no devices available do set null
+        const device = action.payload?.[0] || null
+        listenerApi.dispatch(setSelectedDevice({ device } as SetSelectedDeviceType))
+    },
+})
+
+startListening({
+    predicate: (action) => setSelectedMne.match(action),
+    effect: async (action, listenerApi) => {
+        listenerApi.dispatch(refreshUpdateTimer(Category.TAB as CategoryType))
+    },
 })
 
-// add default selected device if no selected device is set
 startListening({
     predicate: (action) => setDevices.match(action),
     effect: async (action, listenerApi) => {
-        const { device } = listenerApi.getOriginalState() as RootState
-        if (!device.selectedDevice && !!action.payload[0]) {
-            listenerApi.dispatch(setSelectedDevice(action.payload[0]))
-        }
+        listenerApi.dispatch(refreshUpdateTimer(Category.DEVICE as CategoryType))
     },
 })
+
+/**
+ * On startup reset the selected device 
+ */
+startListening({
+    predicate: ({ type }: any) => type === REHYDRATE,
+    effect: async (_, listenerApi) => {
+        const { device: state } = listenerApi.getState() as RootState
+        const device = state.selected?.device
+        if (!device) {
+            return
+        }
+
+        listenerApi.dispatch(setSelectedDevice({ device, options: { bypassCheck: true } } as SetSelectedDeviceType))
+    },
+})
\ No newline at end of file
diff --git a/react-ui/src/components/devices/routines/device.routine.ts b/react-ui/src/components/devices/routines/device.routine.ts
index 10caffa2faa1eb0bdcba5f616c23f36c1721cb30..058f65f9fb7416d1efb55cf7a00e70571b55fb30 100755
--- a/react-ui/src/components/devices/routines/device.routine.ts
+++ b/react-ui/src/components/devices/routines/device.routine.ts
@@ -1,26 +1,39 @@
 import { NetworkElementServiceGetAllFlattenedApiArg, api } from '@api/api'
 import { setDevices } from '@component/devices/reducer/device.reducer'
 import { createAsyncThunk } from '@reduxjs/toolkit'
+import { addRoutine } from '@shared/reducer/routine.reducer'
 import { setUser } from '@shared/reducer/user.reducer'
+import { Category, CategoryType } from '@shared/types/category.type'
 import { RootState } from 'src/stores'
 import { startListening } from '../../../stores/middleware/listener.middleware'
 
 export const FETCH_DEVICE_ACTION = 'subscription/device/fetchDevices'
 
 // continously fetch devices
-const FETCH_DEVICES_INTERVAL = 15000 // in ms
 startListening({
     actionCreator: setUser,
     effect: async (_, listenerApi) => {
-        listenerApi.dispatch(fetchDevicesThunk())
+        listenerApi.dispatch(
+            addRoutine({
+                thunk: fetchDevicesThunk,
+                category: Category.DEVICE as CategoryType,
+                payload: {},
+            })
+        )
     },
 })
 
+const FETCH_DEVICES_INTERVAL = 15000 // in ms
 export const fetchDevicesThunk = createAsyncThunk(FETCH_DEVICE_ACTION, (_, thunkApi) => {
     const { user } = thunkApi.getState() as RootState
 
+    if (!user.user?.roles) {
+        throw new Error('Background device fetching failed! User data is missing. Reload page or logout and login again')
+        // TODO
+    }
+
     const payload: NetworkElementServiceGetAllFlattenedApiArg = {
-        pid: Object.keys(user?.user.roles)[0],
+        pid: Object.keys(user.user.roles)[0],
         timestamp: new Date().getTime().toString(),
     }
 
diff --git a/react-ui/src/components/devices/routines/mne.routine.ts b/react-ui/src/components/devices/routines/mne.routine.ts
index 7de1d18149c35c2bfbe47582d479a5736fa7f1b1..2928693075a75bf7af1044d44a91488b56e10b23 100755
--- a/react-ui/src/components/devices/routines/mne.routine.ts
+++ b/react-ui/src/components/devices/routines/mne.routine.ts
@@ -6,26 +6,42 @@ import {
     setSelectedMne,
 } from '@component/devices/reducer/device.reducer'
 import { createAsyncThunk } from '@reduxjs/toolkit'
-import { addRoutine, CATEGORIES } from '@shared/reducer/routine.reducer'
+import { addRoutine } from '@shared/reducer/routine.reducer'
+import { Category, CategoryType } from '@shared/types/category.type'
 import { RootState } from 'src/stores'
 import { startListening } from '../../../stores/middleware/listener.middleware'
 
 export const FETCH_MNE_ACTION = 'subscription/device/fetchSelectedMNE'
 
-// fetch mne if selected device is set
+
+/**
+ * #0
+ * Trigger fetch MNE (#1)
+ * 
+ * Triggered by a selectedDevice
+ */
 startListening({
-    predicate: (action) => setSelectedDevice.match(action) && !!action.payload,
+    predicate: (action) => setSelectedDevice.match(action) && !!action.payload.device && !action.meta?.skipListener,
     effect: async (action, listenerApi) => {
+
+        const device = action.payload.device
+
         listenerApi.dispatch(
             addRoutine({
                 thunk: fetchSelectedMneThunk,
-                category: CATEGORIES.TAB,
-                payload: action.payload,
+                category: Category.TAB as CategoryType,
+                payload: device,
             })
         )
     },
 })
 
+/**
+ * #1
+ * Fetch MNE
+ * 
+ * Triggered by #0
+ */
 const FETCH_MNE_INTERVAL = 5000 // in ms
 export const fetchSelectedMneThunk = createAsyncThunk(
     FETCH_MNE_ACTION,
@@ -56,7 +72,12 @@ export const fetchSelectedMneThunk = createAsyncThunk(
     }
 )
 
-// save fetched mne
+/**
+ * #2
+ * Received MNE
+ * 
+ * Triggered by #1
+ */
 startListening({
     predicate: (action) => api.endpoints.networkElementServiceGet.matchFulfilled(action),
     effect: async (action, listenerApi) => {
@@ -64,7 +85,12 @@ startListening({
     },
 })
 
-// save fetched mne
+/**
+ * #3
+ * Fetch & receive json
+ * 
+ * Triggered by #2
+ */
 startListening({
     predicate: (action) => setSelectedMne.match(action),
     effect: async (action, listenerApi) => {
diff --git a/react-ui/src/components/devices/view/device.scss b/react-ui/src/components/devices/view/device.scss
index 540cd4d01d67ba3e11b5e8d18c654dceb160ee95..8d4099fc88cacccd6dd4b91d45db664668c5260c 100755
--- a/react-ui/src/components/devices/view/device.scss
+++ b/react-ui/src/components/devices/view/device.scss
@@ -24,11 +24,6 @@
     }
 }
 
-.c-box {
-    padding: 2em !important;
-    padding-top: 1em !important;
-}
-
 .border-right {
     $border-padding: 2em;
 
@@ -57,5 +52,6 @@
     &.active {
         color: map-get($theme-colors, primary);
         font-weight: 500;
+        text-decoration: underline;
     }
 }
diff --git a/react-ui/src/components/devices/view/device.view.table.tsx b/react-ui/src/components/devices/view/device.view.table.tsx
index 312caab60d858b0931e50154c2c6e796e0405e47..1f7221eadb2923a4a70fe81dd156efa4fc49175b 100755
--- a/react-ui/src/components/devices/view/device.view.table.tsx
+++ b/react-ui/src/components/devices/view/device.view.table.tsx
@@ -1,54 +1,66 @@
+import { insertMarkTags } from "@helper/text";
 import { useAppSelector } from "@hooks";
-import { MutableRefObject, useCallback } from "react";
+import DOMPurify from 'dompurify';
+import { MutableRefObject, useCallback, useRef } from "react";
 import { OverlayTrigger, Table, Tooltip } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 import { useDeviceTableViewModel } from "../view_model/device.table.viewmodel";
 
+const cropUUID = (uuid: string): string => {
+    return uuid.substring(0, 3) + "..." + uuid.substring(uuid.length - 3, uuid.length);
+}
+
 export const DeviceViewTable = (searchRef: MutableRefObject<HTMLInputElement>) => {
-    const { devices, pnds, selectedDevice } = useAppSelector(state => state.device);
+    const { devices, pnds, selected: selectedDevice } = useAppSelector(state => state.device);
     const { t } = useTranslation('common');
-    const { trClickHandler } = useDeviceTableViewModel(searchRef);
+    const tableRef = useRef();
+    const { trClickHandler } = useDeviceTableViewModel(searchRef, tableRef);
 
+    const getDeviceTable = useCallback(() => {
+        const search = searchRef.current?.value;
+        let filtered = devices
 
-    const cropUUID = (uuid: string): string => {
-        return uuid.substring(0, 3) + "..." + uuid.substring(uuid.length - 3, uuid.length);
-    }
+        // filter if something is in search
+        if (search) {
+            filtered = devices.filter((device) => {
+                const user = pnds.find(pnd => pnd.id === device.pid);
 
-    const getDeviceTable = useCallback(() => {
-        return devices.filter((device) => {
-            if (!searchRef.current?.value) {
-                return true;
-            }
+                return device.id?.includes(search) ||
+                    device.name?.includes(search) ||
+                    user?.name?.includes(search);
+            })
+        }
 
-            const searchInput = searchRef.current!.value;
+        return filtered.map((device, index) => {
             const user = pnds.find(pnd => pnd.id === device.pid);
 
-            return device.id.includes(searchInput) || device.name.includes(searchInput) || user?.name.includes(searchInput);
-        }).map((device, index) => {
-            const user = pnds.find(pnd => pnd.id === device.pid);
+            const username = user?.name || ''
+            const deviceId = device.id!;
+            const cropedId = cropUUID(deviceId)
+            const devicename = device.name || '';
+
+            const rowData = username + ";" + deviceId + ";" + devicename
 
             return (
-                <tr key={index} onClick={() => trClickHandler(device)} className={selectedDevice?.device.id === device.id ? 'active' : ''}>
-                    <td>{device.name}</td>
-                    <OverlayTrigger overlay={<Tooltip id={device.id}>{device.id}</Tooltip>}>
-                        <td>{cropUUID(device.id)}</td>
+                <tr data-copy-value={rowData} key={index} onClick={() => trClickHandler(device)} className={selectedDevice?.device.id === deviceId ? 'active' : ''}>
+                    <td data-copy-value={devicename} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(devicename, search) : DOMPurify.sanitize(devicename) }}></td>
+                    <OverlayTrigger overlay={<Tooltip id={device.id}>{deviceId}</Tooltip>}>
+                        <td data-copy-value={deviceId} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(cropedId, search) : DOMPurify.sanitize(cropedId) }}></td>
                     </OverlayTrigger>
-                    <td>{user?.name || ''}</td>
-                    <td></td>
+                    <td data-copy-value={username} dangerouslySetInnerHTML={{ __html: search ? insertMarkTags(username, search) : DOMPurify.sanitize(username) }}></td>
                 </tr>
+
             )
         })
     }, [devices, searchRef, pnds, selectedDevice, trClickHandler]);
 
-
     return (
-        <Table striped responsive className="device-table">
+        <Table striped responsive className="device-table" ref={tableRef}>
             <thead>
                 <tr>
                     <th>{t('device.table.header.name')}</th>
                     <th>{t('device.table.header.uuid')}</th>
                     <th>{t('device.table.header.user')}</th>
-                    <th>{t('device.table.header.last_updated')}</th>
                 </tr>
             </thead>
             <tbody>
diff --git a/react-ui/src/components/devices/view/device.view.tabs.tsx b/react-ui/src/components/devices/view/device.view.tabs.tsx
index 2929f9b64855359fa19ea9c0e17d90f5af79bb61..ef8ba120fb4e66dfabaf5b3d81beecee5445c0bd 100755
--- a/react-ui/src/components/devices/view/device.view.tabs.tsx
+++ b/react-ui/src/components/devices/view/device.view.tabs.tsx
@@ -8,7 +8,7 @@ export enum DeviceViewTabValues {
 }
 
 export const DeviceViewTabs = (activeTab: DeviceViewTabValues) => {
-    const { selectedDevice } = useAppSelector(state => state.device);
+    const { selected: selectedDevice } = useAppSelector(state => state.device);
     const { jsonYang } = useDeviceTabsViewModel();
 
     const metadataTab = () => {
@@ -24,8 +24,6 @@ export const DeviceViewTabs = (activeTab: DeviceViewTabValues) => {
             <>
                 {jsonYang &&
                     <JsonViewer json={jsonYang} />
-
-                    //<ReactJson src={selectedDevice.json} name={false} collapsed={true} quotesOnKeys={false} />
                 }
             </>
         );
diff --git a/react-ui/src/components/devices/view/device.view.tsx b/react-ui/src/components/devices/view/device.view.tsx
index 2602463bba8e51a8be7aff14b50008d0c9ad32ce..6bd702bf7d895bf58e679c380edb7e5ebb384a8e 100755
--- a/react-ui/src/components/devices/view/device.view.tsx
+++ b/react-ui/src/components/devices/view/device.view.tsx
@@ -1,33 +1,44 @@
+import { faGripVertical } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { GridLayout } from '@layout/grid.layout/grid.layout';
+import UpdateIndicator from '@layout/grid.layout/update-inidicator.layout/update-indicator.layout';
+import { Category, CategoryType } from '@shared/types/category.type';
 import { useRef } from 'react';
 import { Button, Col, Container, Form, Nav, NavLink, Row } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
 import { useDeviceViewModel } from '../view_model/device.viewmodel';
 import './device.scss';
 import { DeviceViewTable } from './device.view.table';
-import { DeviceViewTabs, DeviceViewTabValues } from './device.view.tabs';
+import { DeviceViewTabValues, DeviceViewTabs } from './device.view.tabs';
 
-function DeviceView() {
+const DeviceView = () => {
     const { t } = useTranslation('common');
     const searchRef = useRef<HTMLInputElement>(null);
     const { activeTab, setActiveTab, handleActiveTabLink } = useDeviceViewModel();
 
     return (
         <div className='m-4 pt-4'>
-            <Container fluid>
-                <Row>
-                    <Col sm={5}>
-                        <Container className='bg-white rounded c-box'>
+            <GridLayout>
+                <>
+                    <div key="device-list">
+                        <Container className='c-box hoverable h-100'>
+                            <UpdateIndicator
+                                category={Category.DEVICE as CategoryType}
+                                updateInterval={15000}
+                            />
+                            <FontAwesomeIcon icon={faGripVertical} className="drag-handle" />
                             <Row>
-                                <Col sm={12} className='mt-4'><h3 className='text-black-50'>{t('device.title')}</h3></Col>
+                                <Col sm={12} className='mt-4'>
+                                    <h3 className='text-black-50'>{t('device.title')}</h3>
+                                </Col>
                             </Row>
-
                             <Row className='align-items-center'>
-                                <Col sm={6}>
+                                <Col xs={12} sm={6}>
                                     <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'>
                                         <Form.Control type="text" placeholder={t('device.search.placeholder')} ref={searchRef} />
                                     </Form.Group>
                                 </Col>
-                                <Col sm={{ span: 3, offset: 3 }} className='pt-2'>
+                                <Col xs={12} sm={6} className='pt-2'>
                                     <Button variant='primary' className='w-100 my-auto'>{t('device.add_device_button')}</Button>
                                 </Col>
                             </Row>
@@ -38,29 +49,44 @@ function DeviceView() {
                                 </Col>
                             </Row>
                         </Container>
-                    </Col>
-                    <Col sm={7}>
-                        <Container className='bg-white rounded c-box'>
+                    </div>
+
+                    <div key="device-details">
+                        <Container className='c-box hoverable h-100'>
+                            <UpdateIndicator
+                                category={Category.TAB as CategoryType}
+                                updateInterval={5000}
+                            />
+                            <FontAwesomeIcon icon={faGripVertical} className="drag-handle" />
                             <Row>
-                                <Col sm={12} className='mt-4'>
+                                <Col xs={12} className='mt-4'>
                                     <Nav className='justify-content-around'>
-                                        <NavLink className={handleActiveTabLink(DeviceViewTabValues.METADATA) + " tab-links"} onClick={() => setActiveTab(DeviceViewTabValues.METADATA)}>{t('device.tabs.metadata.title')}</NavLink>
-                                        <NavLink className={handleActiveTabLink(DeviceViewTabValues.YANGMODEL) + " tab-links"} onClick={() => setActiveTab(DeviceViewTabValues.YANGMODEL)}>{t('device.tabs.yang_model.title')}</NavLink>
+                                        <NavLink
+                                            className={handleActiveTabLink(DeviceViewTabValues.METADATA) + " tab-links"}
+                                            onClick={() => setActiveTab(DeviceViewTabValues.METADATA)}
+                                        >
+                                            {t('device.tabs.metadata.title')}
+                                        </NavLink>
+                                        <NavLink
+                                            className={handleActiveTabLink(DeviceViewTabValues.YANGMODEL) + " tab-links"}
+                                            onClick={() => setActiveTab(DeviceViewTabValues.YANGMODEL)}
+                                        >
+                                            {t('device.tabs.yang_model.title')}
+                                        </NavLink>
                                     </Nav>
                                 </Col>
                             </Row>
-
                             <Row className='align-items-start'>
-                                <Col sm={12} className='pt-2'>
+                                <Col xs={12}>
                                     {DeviceViewTabs(activeTab)}
                                 </Col>
                             </Row>
                         </Container>
-                    </Col>
-                </Row>
-            </Container>
+                    </div>
+                </>
+            </GridLayout>
         </div>
-    )
-}
+    );
+};
 
-export default DeviceView
+export default DeviceView;
\ No newline at end of file
diff --git a/react-ui/src/components/devices/view_model/device.table.viewmodel.ts b/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
index df75953288d659c17a53a744c7c4ba015f0dcab9..4b328d63955f3827a3f6cdabbf83951505fe7dfa 100755
--- a/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
+++ b/react-ui/src/components/devices/view_model/device.table.viewmodel.ts
@@ -1,32 +1,100 @@
 import { Device, setSelectedDevice } from "@component/devices/reducer/device.reducer";
+import { faCopy } from "@fortawesome/free-solid-svg-icons";
 import { useAppDispatch } from "@hooks";
+import { useMenu } from "@provider/menu/menu.provider";
+import { useUtils } from "@provider/utils.provider";
 import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { toast } from "react-toastify";
 
-export const useDeviceTableViewModel = (searchRef) => {
+export const useDeviceTableViewModel = (searchRef, tableRef) => {
     const [searchTerm, setSearchTerm] = useState('');
     const dispatch = useAppDispatch();
+    const { subscribe } = useMenu();
+    const { toClipboard } = useUtils();
+    const { t } = useTranslation('common');
+
+
+    const registerMenuOptions = () => {
+        const subscription = subscribe({
+            target: tableRef.current,
+            actions: [
+                {
+                    key: t('device.table.actions.copy'),
+                    icon: faCopy,
+                    action: (clickedElement) => {
+                        if (clickedElement) {
+                            const text = clickedElement.dataset.copyValue
+                            if (!text) {
+                                toast.warn(t('global.toast.copied_failed'))
+                                return
+                            }
+
+
+                            toClipboard(text)
+                        }
+                    }
+                },
+
+                {
+                    key: t('device.table.actions.copy_row'),
+                    icon: faCopy,
+                    action: (clickedElement) => {
+                        let parent = clickedElement;
+                        while (parent && parent.tagName !== 'TR') {
+                            parent = parent.parentNode;
+                        }
+
+                        const text = parent.dataset.copyValue
+                        if (!text) {
+                            toast.warn(t('global.toast.copied_failed'))
+                            return
+                        }
+                        toClipboard(text)
+                    }
+                }
+            ]
+        })
+
+        return () => {
+            subscription.unsubscribe()
+        }
+    }
+
+    // seperate use effect to rerun this after tableref and subscribe are initialized
+    useEffect(() => {
+        if (!subscribe || !tableRef.current) {
+            return
+        }
+
+        const unsubscribe = registerMenuOptions()
+
+        return () => {
+            unsubscribe()
+        }
+    }, [tableRef, subscribe])
 
 
     useEffect(() => {
+        if (!searchRef.current) {
+            return
+        }
+
         const handleSearchChange = () => {
-            if (searchRef.current) {
-                setSearchTerm(searchRef.current.value);
-            }
+            setSearchTerm(searchRef.current.value);
         };
 
-        if (searchRef.current) {
-            searchRef.current.addEventListener('input', handleSearchChange);
-        }
+        searchRef.current.addEventListener('input', handleSearchChange);
 
         return () => {
             if (searchRef.current) {
                 searchRef.current.removeEventListener('input', handleSearchChange);
             }
         };
-    }, []);
+    }, [searchRef]);
 
     const trClickHandler = (device: Device) => {
-        dispatch(setSelectedDevice(device));
+        dispatch(setSelectedDevice({ device }));
     }
 
 
diff --git a/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts b/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
index 4a60567b68e78e571577d7f74fb0729df47008cb..af4cc3abb9f0b2e207acd5c71fbb48f4a0562ca7 100755
--- a/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
+++ b/react-ui/src/components/devices/view_model/device.tabs.viewmodel.ts
@@ -7,7 +7,7 @@ export enum DeviceViewTabValues {
 }
 
 export const useDeviceTabsViewModel = () => {
-    const { selectedDevice } = useAppSelector((state) => state.device)
+    const { selected: selectedDevice } = useAppSelector((state) => state.device)
 
     const getYangModelJSON = (): JSON | null => {
         if (!selectedDevice?.json) {
diff --git a/react-ui/src/components/devices/view_model/device.viewmodel.ts b/react-ui/src/components/devices/view_model/device.viewmodel.ts
index 9a0fbe17a0b8f3bf0aa5c00e8ef10a9d56b4a08d..1cce2d59a2e984721d4730b9e98c310191a70937 100755
--- a/react-ui/src/components/devices/view_model/device.viewmodel.ts
+++ b/react-ui/src/components/devices/view_model/device.viewmodel.ts
@@ -7,7 +7,7 @@ export const useDeviceViewModel = () => {
     const { activeTab } = useAppSelector((state) => state.device)
     const dispatch = useAppDispatch()
 
-    useEffect(() => {}, [])
+    useEffect(() => { }, [])
 
     const handleActiveTabLink = (tabLink: DeviceViewTabValues) => {
         return activeTab === tabLink ? 'active' : ''
diff --git a/react-ui/src/components/login/layouts/login.layout.tsx b/react-ui/src/components/login/layouts/login.layout.tsx
index 556dc6de436397b283bd426041b1d08749cf505a..58e8fc99133d8674b57c7d39f06e40da2c9f03e4 100755
--- a/react-ui/src/components/login/layouts/login.layout.tsx
+++ b/react-ui/src/components/login/layouts/login.layout.tsx
@@ -20,4 +20,6 @@ export const LoginLayout = () => {
     return (
         <LoginView>{outlet}</LoginView>
     )
-}
\ No newline at end of file
+}
+
+export default LoginLayout
\ No newline at end of file
diff --git a/react-ui/src/components/login/view/login.view.tsx b/react-ui/src/components/login/view/login.view.tsx
index 03d7406f42bc37a78f9e6047f4aa70758e8b8258..62c84eeb8b07dae2c3e2aa180d36a14d8761e18f 100755
--- a/react-ui/src/components/login/view/login.view.tsx
+++ b/react-ui/src/components/login/view/login.view.tsx
@@ -1,10 +1,10 @@
-import logo from '@assets/logo.svg'
-import { BasicProp } from '@helper/interfaces'
+import { BasicProp } from '@shared/types/interfaces.type'
 import React, { useRef } from 'react'
 import { Alert, Button, Col, Container, Form, Image, Row, Spinner } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import useLoginViewModel from '../viewmodel/login.viewmodel'
 import './login.scss'
+import logo from '/public/logo.svg'
 
 const LoginView: React.FC<BasicProp> = () => {
     const { t } = useTranslation('common')
diff --git a/react-ui/src/components/login/viewmodel/login.viewmodel.ts b/react-ui/src/components/login/viewmodel/login.viewmodel.ts
index 60c39b55ab7efe77b90fe5c449504f1d1193e2e6..fabb0a8613cfee92f864beef15a998dda159238f 100755
--- a/react-ui/src/components/login/viewmodel/login.viewmodel.ts
+++ b/react-ui/src/components/login/viewmodel/login.viewmodel.ts
@@ -7,9 +7,9 @@ export interface PageLoginState {
 }
 
 export default function useLoginViewModel() {
-    const {login, loginProperties} = useAuth();
-    const {isLoading: loginLoading, error: loginError, reset: resetLogin} = loginProperties!;
-    
+    const { login, loginProperties } = useAuth();
+    const { isLoading: loginLoading, error: loginError, reset: resetLogin } = loginProperties;
+
     const [localFormState, updateLocalFormState] = useState({
         submitted: false,
         valid: false,
diff --git a/react-ui/src/i18n/locales/en/translations.json b/react-ui/src/i18n/locales/en/translations.json
index 46b76563c7d78355e9484e49d344f4132bf3766d..53444b9e0d6745f364a559342164dca89d7abc0f 100755
--- a/react-ui/src/i18n/locales/en/translations.json
+++ b/react-ui/src/i18n/locales/en/translations.json
@@ -6,7 +6,8 @@
                 "empty_field": "This field can´t be empty"
             },
             "toast": {
-                "copied": "Copied to clipboard"
+                "copied": "Copied to clipboard",
+                "copied_failed": "Copying to clipboard failed"
             },
             "menu_item": {
                 "logout": "Logout"
@@ -35,6 +36,10 @@
                     "uuid": "UUID",
                     "user": "User",
                     "last_updated": "Last updated"
+                },
+                "actions": {
+                    "copy": "Copy",
+                    "copy_row": "Copy row"
                 }
             },
             "search": {
@@ -48,6 +53,9 @@
                 "yang_model": {
                     "title": "YANG Model"
                 }
+            },
+            "box": {
+                "lastUpdate": "Last updated {{seconds}} seconds ago"
             }
         },
         "protected": {
diff --git a/react-ui/src/index.scss b/react-ui/src/index.scss
index 8dd280e6436cf6df71772494e87cdd950872f16b..5c9f184b7fc394991e9895411be67b1f31f36722 100755
--- a/react-ui/src/index.scss
+++ b/react-ui/src/index.scss
@@ -1,4 +1,4 @@
-@import './shared/style/index.scss';
+@import "./shared/style/index.scss";
 
 body {
     margin: 0;
diff --git a/react-ui/src/index.tsx b/react-ui/src/index.tsx
index 8383248cea22fc89c4ba1c9f5248458f7fa150ec..3697efd07379da0126d042b824b9c3d857bb6fed 100755
--- a/react-ui/src/index.tsx
+++ b/react-ui/src/index.tsx
@@ -16,6 +16,7 @@ import { router } from './routes'
 import './shared/icons/icons'
 import { persistor, store } from './stores'
 
+window.env = window.location.hostname === 'localhost' ? 'development' : 'production';
 
 ReactDOM.createRoot(document.getElementById("root")).render(
     <React.StrictMode>
diff --git a/react-ui/src/routes.tsx b/react-ui/src/routes.tsx
index 368df55a472bdbc20bc2dd34bb9fb1f33b43047f..a476feaaedfd8922a1841bd6f877792416856f13 100755
--- a/react-ui/src/routes.tsx
+++ b/react-ui/src/routes.tsx
@@ -1,21 +1,50 @@
 import { BasicLayout } from "@layout/basic.layout";
 import { ProtectedLayout } from "@layout/protected.layout/protected.layout";
+import DelayedRender, { SplashScreen } from "@utils/loading-fallback";
+import { lazy, Suspense } from 'react';
 import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom";
-import DeviceView from "./components/devices/view/device.view";
-import { LoginLayout } from "./components/login/layouts/login.layout";
 
 export const DEVICE_URL = '/device/';
 export const LOGIN_URL = '/login';
 
+// Lazy load components
+const DeviceView = lazy(() => import('./components/devices/view/device.view'));
+const LoginLayout = lazy(() => import('./components/login/layouts/login.layout'));
 
 export const router = createBrowserRouter(
     createRoutesFromElements(
         <Route element={<BasicLayout />}>
-            <Route path={LOGIN_URL} element={<LoginLayout />} />
+            <Route
+                path={LOGIN_URL}
+                element={
+                    <Suspense fallback={null}>
+                        <DelayedRender>
+                            <LoginLayout />
+                        </DelayedRender>
+                    </Suspense>
+                }
+            />
             <Route element={<ProtectedLayout />}>
-                <Route path={DEVICE_URL} element={<DeviceView />} />
-                <Route path="/" element={<Navigate to={DEVICE_URL} replace={true} />} />
+                <Route
+                    path={DEVICE_URL}
+                    element={
+                        <DelayedRender
+                            loading={{
+                                minimumLoadingTime: 1000,
+                                component: SplashScreen
+                            }}
+                        >
+                            <Suspense fallback={null}>
+                                <DeviceView />
+                            </Suspense>
+                        </DelayedRender>
+                    }
+                />
+                <Route
+                    path="/"
+                    element={<Navigate to={DEVICE_URL} replace={true} />}
+                />
             </Route>
-        </Route>
+        </Route >
     )
-)
\ No newline at end of file
+);
\ No newline at end of file
diff --git a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
index 720f75c7d26b4ffd181707c46ff42502168a72c0..33e52a39788fe1120c18972c14181a6abaf8067b 100755
--- a/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
+++ b/react-ui/src/shared/components/json_viewer/view/json_viewer.view.tsx
@@ -1,5 +1,6 @@
 import { faAlignRight, faPenToSquare, faTrashCan } from "@fortawesome/free-solid-svg-icons"
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import { insertMarkTags } from "@helper/text"
 import DOMPurify from 'dompurify'
 import React, { Suspense, useMemo, useRef } from "react"
 import { Form, Table } from "react-bootstrap"
@@ -30,13 +31,6 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
         )
     }, [breadcrumbs])
 
-    const insertMarkTags = (text: string, search: string): string => {
-        const start = text.indexOf(search)
-        const end = start + search.length
-
-        return DOMPurify.sanitize(text.substring(0, start)) + "<span class='highlight'>" + DOMPurify.sanitize(search) + "</span>" + DOMPurify.sanitize(text.substring(end, text.length))
-    }
-
     const renderInner = (innerJson: JSON, nested: number = 0, parentKey: string = "", path: string = "/network-instance/0/"): JSX.Element => {
         path += parentKey + (parentKey === "" ? "" : "/")
 
@@ -130,7 +124,7 @@ export const JsonViewer = ({ json }: JsonViewerProbs) => {
     const searchHTML = () => {
         return (
             <>
-                <Form.Group controlId='device.search' className='p-0 mx-1 pt-2'>
+                <Form.Group controlId='json_viewer.search' className='p-0 mx-1 pt-2'>
                     <Form.Control type="text" placeholder={t('device.search.placeholder')} ref={search} />
                 </Form.Group>
             </>
diff --git a/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx b/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx
index 6c3ac78f306d0ff0b1c09263a4d791795757b51c..f0bc922126f6752e9fcb0182391bd3deea461bb7 100644
--- a/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx
+++ b/react-ui/src/shared/components/json_viewer/viewmodel/json_viewer.viewmodel.tsx
@@ -67,29 +67,31 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy
     }
 
     const registerMenuOptions = () => {
-        if (container.current) {
-            const subscription = subscribe({
-                target: container.current,
-                actions: [
-                    {
-                        key: t('json_viewer.copy'),
-                        icon: faCopy,
-                        action: (clickedElement) => {
-                            let parent = clickedElement;
-                            while (parent && parent.tagName !== 'TR') {
-                                parent = parent.parentNode;
-                            }
-
-                            const copyValue = parent.dataset.copyValue
-                            toClipboard(copyValue)
+        if (!container.current) {
+            return () => { }
+        }
+
+        const subscription = subscribe({
+            target: container.current,
+            actions: [
+                {
+                    key: t('json_viewer.copy'),
+                    icon: faCopy,
+                    action: (clickedElement) => {
+                        let parent = clickedElement;
+                        while (parent && parent.tagName !== 'TR') {
+                            parent = parent.parentNode;
                         }
+
+                        const copyValue = parent.dataset.copyValue
+                        toClipboard(copyValue)
                     }
-                ]
-            })
+                }
+            ]
+        })
 
-            return () => {
-                subscription.unsubscribe();
-            }
+        return () => {
+            subscription.unsubscribe();
         }
     }
 
@@ -137,7 +139,7 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy
     }, [searchTerm])
 
     useEffect(() => {
-        registerMenuOptions();
+        const unsubscribe = registerMenuOptions();
 
         if (search.current) {
             search.current.addEventListener('input', handleSearchInput)
@@ -147,6 +149,7 @@ export const useJsonViewer = ({ json, search, container }: JsonViewerViewModelTy
             if (search.current) {
                 search.current.removeEventListener('input', handleSearchInput)
             }
+            unsubscribe()
         }
     }, [])
 
diff --git a/react-ui/src/shared/helper/debug.ts b/react-ui/src/shared/helper/debug.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db10a0979ac9b5acfab21af71fc4c1dc2595fbe3
--- /dev/null
+++ b/react-ui/src/shared/helper/debug.ts
@@ -0,0 +1,11 @@
+export const warnMessage = (message: string) => {
+    if (window?.env === 'development') {
+        console.warn("Debug: \n" + message)
+    }
+}
+
+export const infoMessage = (message: string) => {
+    if (window?.env === 'development') {
+        console.info("Info: \n" + message)
+    }
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/helper/text.ts b/react-ui/src/shared/helper/text.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6aee13790561eefcfe347fb2e42a79c5ef84cfec
--- /dev/null
+++ b/react-ui/src/shared/helper/text.ts
@@ -0,0 +1,12 @@
+
+import DOMPurify from 'dompurify'
+
+export const insertMarkTags = (text: string, search: string): string => {
+    const start = text.indexOf(search)
+    if (start === -1) {
+        return DOMPurify.sanitize(text)
+    }
+    const end = start + search.length
+
+    return DOMPurify.sanitize(text.substring(0, start)) + "<span class='highlight'>" + DOMPurify.sanitize(search) + "</span>" + DOMPurify.sanitize(text.substring(end, text.length))
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/icons/icons.ts b/react-ui/src/shared/icons/icons.ts
index 00021aa116faae88600fd06ab461aabd41aee9bb..9c8791cc90cdd443a18fa06648ed2802b9a28b03 100755
--- a/react-ui/src/shared/icons/icons.ts
+++ b/react-ui/src/shared/icons/icons.ts
@@ -1,4 +1,4 @@
 import { library } from '@fortawesome/fontawesome-svg-core'
-import { faSpinner, fas } from '@fortawesome/free-solid-svg-icons'
+import { faSpinner } from '@fortawesome/free-solid-svg-icons'
 
-library.add(fas, faSpinner)
\ No newline at end of file
+library.add(faSpinner)
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/grid.layout/grid.layout.scss b/react-ui/src/shared/layouts/grid.layout/grid.layout.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c90375d371f5e78827676f9520f03b1b388a5cc4
--- /dev/null
+++ b/react-ui/src/shared/layouts/grid.layout/grid.layout.scss
@@ -0,0 +1,81 @@
+@import "/src/shared/style/colors.scss";
+
+.drag-handle {
+    position: absolute;
+    top: 0;
+    right: 0;
+    padding: 10px;
+    cursor: grab;
+    color: map-get($theme-colors, "dark");
+    background-color: lighten(map-get($theme-colors, primary), 38%);
+    border-radius: 0 0.25rem 0 0.25rem;
+    border-left: 1px solid lighten(map-get($theme-colors, dark), 35%);
+    border-bottom: 1px solid lighten(map-get($theme-colors, dark), 35%);
+    z-index: 10;
+
+    &:hover {
+        color: map-get($theme-colors, primary);
+        background-color: lighten(map-get($theme-colors, primary), 35%);
+    }
+
+    &:active {
+        cursor: grabbing;
+    }
+}
+
+.react-grid-item {
+    min-height: 600px !important;
+
+    &.react-draggable-dragging {
+        z-index: 100;
+
+        .drag-handle {
+            cursor: grabbing;
+        }
+    }
+}
+
+.react-grid-layout {
+    width: 100% !important;
+}
+
+.react-grid-item.react-grid-placeholder {
+    background: lighten(map-get($theme-colors, primary), 30%) !important;
+    opacity: 0.2;
+    transition-duration: 100ms;
+    z-index: 2;
+    border-radius: 4px;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    -o-user-select: none;
+    user-select: none;
+}
+
+.react-grid-item {
+    /* Hide resize handle by default */
+    .react-resizable-handle-se {
+        opacity: 0;
+        transition: opacity 0.2s ease-in-out;
+    }
+
+    /* Show resize handle on container hover */
+    &:hover .react-resizable-handle-se {
+        opacity: 1;
+    }
+}
+
+/* Style the resize handle */
+.react-resizable-handle-se {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    width: 20px;
+    height: 20px;
+    background-image: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiPjxwYXRoIGQ9Ik02IDZIMHYtNmg2djZ6TTUgMUgxdjRoNFYxeiIgZmlsbD0iIzk5OTk5OSIvPjwvc3ZnPg==");
+    background-position: bottom right;
+    background-repeat: no-repeat;
+    background-size: 10px 10px;
+    cursor: se-resize;
+    z-index: 10;
+}
diff --git a/react-ui/src/shared/layouts/grid.layout/grid.layout.tsx b/react-ui/src/shared/layouts/grid.layout/grid.layout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c184e655b3ba7fa74d951cda2cd6a4ec421691d6
--- /dev/null
+++ b/react-ui/src/shared/layouts/grid.layout/grid.layout.tsx
@@ -0,0 +1,61 @@
+import React, { ReactElement, useEffect, useState } from 'react';
+import { Responsive, WidthProvider } from 'react-grid-layout';
+import 'react-grid-layout/css/styles.css';
+import 'react-resizable/css/styles.css';
+import './grid.layout.scss';
+
+const ResponsiveGridLayout = WidthProvider(Responsive);
+
+interface GridLayoutProps {
+    children: ReactElement;
+}
+
+export const GridLayout: React.FC<GridLayoutProps> = ({ children }) => {
+    const rowHeight = 50;
+    const [mounted, setMounted] = useState(false);
+    const layouts = {
+        lg: [
+            { i: 'device-list', x: 0, y: 0, w: 1, h: 1, minW: 1, minH: 1 },
+            { i: 'device-details', x: 2, y: 0, w: 2, h: 1, minW: 2, minH: 1 }
+        ]
+    };
+
+    useEffect(() => {
+        setMounted(true);
+        // Force layout recalculation after mount
+        window.dispatchEvent(new Event('resize'));
+    }, []);
+
+    const gridItems = React.Children.map(children.props.children, (child, index) => {
+        if (!React.isValidElement(child)) return null;
+
+        return React.cloneElement(child, {
+            key: index === 0 ? 'device-list' : 'device-details',
+            'data-grid': layouts.lg[index]
+        });
+    });
+
+    return (
+        <div style={{ display: mounted ? 'block' : 'none' }}>
+            <ResponsiveGridLayout
+                className="layout"
+                layouts={layouts}
+                breakpoints={{ lg: 996, sm: 480 }}
+                cols={{ lg: 4, sm: 3 }}
+                rowHeight={rowHeight}
+                margin={[20, 20]}
+                draggableHandle=".drag-handle"
+                isDraggable={true}
+                isResizable={true}
+                preventCollision={true}
+                compactType={null}
+                useCSSTransforms={mounted}
+                resizeHandles={['se']} // Only show resize handle in bottom right corner
+            >
+                {gridItems}
+            </ResponsiveGridLayout>
+        </div>
+    );
+};
+
+export default GridLayout;
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.layout.tsx b/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.layout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d71bb6cce17eefb53d7ddcdcda9db99545bf6b86
--- /dev/null
+++ b/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.layout.tsx
@@ -0,0 +1,52 @@
+import { faCircle } from '@fortawesome/free-solid-svg-icons'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import React, { useState } from 'react'
+import { Overlay, Tooltip } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import { CategoryType } from '../types'
+import { useUpdateIndicatorViewModel } from './update-indicator.viewmodel'
+
+interface UpdateIndicatorProps {
+    category: CategoryType
+    updateInterval: number
+}
+
+const UpdateIndicator: React.FC<UpdateIndicatorProps> = ({ category, updateInterval }) => {
+    const [showTooltip, setShowTooltip] = useState(false)
+    const { t } = useTranslation('common')
+    const target = React.useRef(null)
+    const { secondsSinceUpdate, getStatusColor } = useUpdateIndicatorViewModel(category)
+
+    return (
+        <div
+            className="position-absolute"
+            style={{
+                top: 0,
+                right: '40px',
+                padding: '10px',
+                zIndex: 10
+            }}
+        >
+            <div
+                ref={target}
+                onMouseEnter={() => setShowTooltip(true)}
+                onMouseLeave={() => setShowTooltip(false)}
+                style={{ cursor: 'pointer' }}
+            >
+                <FontAwesomeIcon
+                    icon={faCircle}
+                    className={getStatusColor(updateInterval)}
+                    size="sm"
+                />
+            </div>
+
+            <Overlay target={target.current} show={showTooltip} placement="bottom">
+                <Tooltip id="update-tooltip">
+                    {t('device.box.lastUpdate', { seconds: secondsSinceUpdate })}
+                </Tooltip>
+            </Overlay>
+        </div>
+    )
+}
+
+export default UpdateIndicator
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.viewmodel.tsx b/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.viewmodel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bb91b0b17e1c4e815d92d14420f0022ddaf819e2
--- /dev/null
+++ b/react-ui/src/shared/layouts/grid.layout/update-inidicator.layout/update-indicator.viewmodel.tsx
@@ -0,0 +1,39 @@
+import { useAppSelector } from "@hooks"
+import { CategoryType } from "@shared/types/category.type"
+import { useEffect, useState } from 'react'
+
+export const useUpdateIndicatorViewModel = (category: CategoryType) => {
+    const { thunks } = useAppSelector((state) => state.routine)
+    const [secondsSinceUpdate, setSecondsSinceUpdate] = useState<number>(-1)
+
+    useEffect(() => {
+        const updateTimer = () => {
+            const lastupdate = thunks[category]?.lastupdate
+            if (lastupdate) {
+                setSecondsSinceUpdate(Math.round((Date.now() - lastupdate) / 1000))
+            } else {
+                setSecondsSinceUpdate(-1)
+            }
+        }
+
+        // Initial update
+        updateTimer()
+
+        // Set up interval for updates
+        const intervalId = setInterval(updateTimer, 1000)
+
+        return () => clearInterval(intervalId)
+    }, [category, thunks])
+
+    const getStatusColor = (updateInterval: number) => {
+        const updateIntervalSeconds = updateInterval / 1000
+        if (secondsSinceUpdate > updateIntervalSeconds * 0.9) return "text-primary"
+        if (secondsSinceUpdate > updateIntervalSeconds * 1.3) return "text-danger"
+        return "text-bg-primary"
+    }
+
+    return {
+        secondsSinceUpdate,
+        getStatusColor
+    }
+}
\ No newline at end of file
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.scss b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
index 07b38d5a73d1102bebf4e4b0f82e8cc2bc10a799..52429031b69b6ef664707c5397f67153c04b08f8 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.scss
@@ -1,7 +1,5 @@
 @import "/src/shared/style/colors.scss";
 
-$sidebar-width: 4.5em;
-
 .head-links {
     text-decoration: none;
     color: map-get($theme-colors, dark);
@@ -10,7 +8,6 @@ $sidebar-width: 4.5em;
 
     &:hover {
         color: map-get($theme-colors, primary);
-        font-weight: 600;
     }
 
     &.active {
@@ -19,11 +16,46 @@ $sidebar-width: 4.5em;
     }
 }
 
-.sidebar {
-    width: $sidebar-width;
-    height: 100vh;
+#navbar {
+    padding: 1em !important;
 }
 
-.main-content {
-    margin-left: $sidebar-width;
+// Add these styles to your protected.layout.scss
+nav {
+    border-radius: $border-radius $border-radius;
+    padding: 0 !important;
+
+    .head-links {
+        text-decoration: none;
+        color: map-get($theme-colors, "dark");
+        padding: 8px 16px;
+        margin: 0 4px;
+        border-radius: 12px;
+        transition: all 0.2s ease;
+
+        &:hover {
+            background-color: map-get($theme-colors, "bg-primary");
+        }
+
+        &.active {
+            color: map-get($theme-colors, "primary");
+            background-color: map-get($theme-colors, "primary::hover");
+        }
+    }
+
+    .dropdown-menu {
+        border-radius: $border-radius;
+        box-shadow: $box-shadow;
+        border: none;
+        padding: 8px;
+
+        .dropdown-item {
+            border-radius: 8px;
+            padding: 8px 16px;
+
+            &:hover {
+                background-color: map-get($theme-colors, "bg-primary");
+            }
+        }
+    }
 }
diff --git a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
index c7e13c6fe55a2f874c6eeafbc956305986051b63..dbcb49192988956ac0aa2eafd4c92575f0364c5c 100755
--- a/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
+++ b/react-ui/src/shared/layouts/protected.layout/protected.layout.tsx
@@ -1,17 +1,16 @@
-import logo from '@assets/logo.svg';
-import { fetchPnds } from '@component/devices/reducer/device.reducer';
 import { faCircleUser, faRightFromBracket } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { useAppDispatch, useAppSelector } from '@hooks';
 import { useAuth } from "@provider/auth.provider";
 import { MenuProvider } from '@provider/menu/menu.provider';
 import { DEVICE_URL, LOGIN_URL } from '@routes';
-import { fetchUser } from '@shared/reducer/user.reducer';
+import { fetchPnds, fetchUser } from '@shared/routine/user.routine';
 import React, { useEffect } from "react";
-import { Dropdown } from "react-bootstrap";
+import { Col, Container, Dropdown, Row } from "react-bootstrap";
 import { useTranslation } from "react-i18next";
 import { Link, Outlet, useNavigate } from "react-router-dom";
 import "./protected.layout.scss";
+import logo from '/public/logo.png';
 
 
 export const ProtectedLayout = () => {
@@ -70,48 +69,44 @@ export const ProtectedLayout = () => {
     }
   );
 
-  const VerticalSidebar = () => {
-    return (
-      <div className="d-flex fixed-top flex-column flex-shrink-0 bg-white sidebar justify-content-end border-end border-dark py-3 z-2">
-        <FontAwesomeIcon className="clickable icon" icon={faRightFromBracket} onClick={logout} size="2x" />
-      </div>
-    )
-  }
-
   const HorizontalNavbar = () => {
     return (
-      <nav className="bg-white border-bottom border-dark py-2 d-flex align-items-center z-3 position-relative">
-        <Link to="/"><img src={logo} className="mx-4 me-5" width={25} alt="logo" /></Link>
-        <Link className={"head-links" + handleActiveLink(DEVICE_URL)} to="/">{t('protected.link.device_list')}</Link>
-        <Link className={"head-links" + handleActiveLink('/map')} to="/">{t('protected.link.map')}</Link>
-        <Link className={"head-links" + handleActiveLink('/configuration_management')} to="/">{t('protected.link.configuration_mgmt')}</Link>
+      <Container fluid>
+        <Row>
+          <Col>
+            <nav id="navbar" className="bg-white mx-4 mt-4 d-flex align-items-center c-box">
+              <Link to="/"><img src={logo} className="mx-4" width={45} alt="logo" /></Link>
+              <Link className={"head-links" + handleActiveLink(DEVICE_URL)} to="/">{t('protected.link.device_list')}</Link>
+              <Link className={"head-links" + handleActiveLink('/map')} to="/">{t('protected.link.map')}</Link>
+              <Link className={"head-links" + handleActiveLink('/configuration_management')} to="/">{t('protected.link.configuration_mgmt')}</Link>
 
-        <Dropdown className="ms-auto px-3">
-          <Dropdown.Toggle as={UserIconToggle}>
-            <FontAwesomeIcon icon={faCircleUser} className="icon clickable" />
-          </Dropdown.Toggle>
+              <Dropdown className="ms-auto px-3">
+                <Dropdown.Toggle as={UserIconToggle}>
+                  <FontAwesomeIcon icon={faCircleUser} className="clickable" size="2x" />
+                </Dropdown.Toggle>
 
-          <Dropdown.Menu as={UserIconMenu}>
-            <Dropdown.Item eventKey="1">{user?.name}</Dropdown.Item>
-            <hr />
-            <Dropdown.Item eventKey="1">
-              <Link className="text-decoration-none text-reset" to="/">{t('protected.link.settings')}</Link>
-            </Dropdown.Item>
-          </Dropdown.Menu>
-        </Dropdown>
-      </nav>
+                <Dropdown.Menu as={UserIconMenu}>
+                  <Dropdown.Item eventKey="1">{user?.name}</Dropdown.Item>
+                  <hr />
+                  <Dropdown.Item eventKey="2">
+                    <Link className="text-decoration-none text-reset" to="/">{t('protected.link.settings')}</Link>
+                  </Dropdown.Item>
+                  <Dropdown.Item eventKey="3" onClick={logout}>
+                    <Link className="text-decoration-none text-reset" to="/"><FontAwesomeIcon className="clickable" icon={faRightFromBracket} /><span className="ms-1">{t('global.menu_item.logout')}</span></Link>
+                  </Dropdown.Item>
+                </Dropdown.Menu>
+              </Dropdown>
+            </nav>
+          </Col>
+        </Row>
+      </Container>
     )
   }
 
   return (
-    <div>
-      <MenuProvider>
-        {HorizontalNavbar()}
-        {VerticalSidebar()}
-        <div className='main-content'>
-          <Outlet />
-        </div>
-      </MenuProvider>
-    </div>
+    <MenuProvider>
+      {HorizontalNavbar()}
+      <Outlet />
+    </MenuProvider>
   )
 };
\ No newline at end of file
diff --git a/react-ui/src/shared/provider/auth.provider.tsx b/react-ui/src/shared/provider/auth.provider.tsx
index 69bdccbdfbb3b6db3295d20a137e56a57d8504f2..77219bdce1547fc11b889eb444ae6d6a9eb5eb6a 100755
--- a/react-ui/src/shared/provider/auth.provider.tsx
+++ b/react-ui/src/shared/provider/auth.provider.tsx
@@ -1,8 +1,8 @@
 import { AuthServiceLoginApiArg, AuthServiceLoginApiResponse, useAuthServiceLoginMutation } from "@api/api";
 import { getCookieValue } from "@helper/coookie";
-import { BasicProp } from "@helper/interfaces";
 import { useAppDispatch, useAppSelector } from "@hooks";
 import { DEVICE_URL, LOGIN_URL } from "@routes";
+import { BasicProp } from "@shared/types/interfaces.type";
 import { jwtDecode } from "jwt-decode";
 import { createContext, useContext, useEffect, useMemo } from "react";
 import { useNavigate } from "react-router-dom";
diff --git a/react-ui/src/shared/provider/menu/menu.provider.tsx b/react-ui/src/shared/provider/menu/menu.provider.tsx
index a91f46639af28e2db261a3089f62e0a79fc13c41..f3af8e5c13573e8c317eb27e433ebf63872e093f 100644
--- a/react-ui/src/shared/provider/menu/menu.provider.tsx
+++ b/react-ui/src/shared/provider/menu/menu.provider.tsx
@@ -1,7 +1,7 @@
 import { faRightFromBracket, IconDefinition } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { BasicProp } from "@helper/interfaces";
 import { useAuth } from "@provider/auth.provider";
+import { BasicProp } from "@shared/types/interfaces.type";
 import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
 import { useTranslation } from "react-i18next";
 import './menu.provider.scss';
@@ -19,13 +19,11 @@ type Action = {
 }
 
 interface MenuProviderType {
-    subscribe: (value: SubscriptionValue) => MenuSubscription
+    subscribe: ((value: SubscriptionValue) => MenuSubscription) | null;
 }
 
 const MenuContext = createContext<MenuProviderType>({
-    subscribe: function (): MenuSubscription {
-        throw new Error("Function not implemented.");
-    }
+    subscribe: null
 })
 
 interface SubscriptionValue {
@@ -33,11 +31,16 @@ interface SubscriptionValue {
     actions: Array<Action>
 }
 
+interface SubscriptionMap {
+    [id: string]: SubscriptionValue
+}
+
 
 export const MenuProvider: React.FC<BasicProp> = ({ children }) => {
     const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
     const [showMenu, setShowMenu] = useState(false);
-    const [subscribedTargets, setSubscribedTargets] = useState<Array<SubscriptionValue>>([])
+    const [subscribedTargets, setSubscribedTargets] = useState<SubscriptionMap>({});
+
     const { logout } = useAuth()
 
     const { t } = useTranslation('common')
@@ -61,12 +64,15 @@ export const MenuProvider: React.FC<BasicProp> = ({ children }) => {
 
     const handleContextMenu = (event: React.MouseEvent<HTMLElement>) => {
         event.preventDefault();
-        const targets = subscribedTargets.filter(({ target }) => target.contains(event.target as HTMLElement))
+
+        const targets = Object.values(subscribedTargets).filter(
+            ({ target }) => target.contains(event.target as HTMLElement)
+        );
 
         setMenuPosition({ top: event.pageY, left: event.pageX });
-        setMenuItems(targets)
-        setClickedHtmlElement(event.target as HTMLElement)
-        displayMenu()
+        setMenuItems(targets);
+        setClickedHtmlElement(event.target as HTMLElement);
+        displayMenu();
     };
 
     const displayMenu = () => {
@@ -90,20 +96,27 @@ export const MenuProvider: React.FC<BasicProp> = ({ children }) => {
 
     const value = useMemo<MenuProviderType>(() => {
         return {
-            subscribe(target) {
-                const index = subscribedTargets.length;
+            subscribe(target: SubscriptionValue) {
+                const subscriptionId = crypto.randomUUID(); // Generate unique ID
 
-                setSubscribedTargets([...subscribedTargets, target])
+                setSubscribedTargets(prev => ({
+                    ...prev,
+                    [subscriptionId]: target
+                }));
 
                 const subscription: MenuSubscription = {
                     unsubscribe() {
-                        setSubscribedTargets([...subscribedTargets.splice(index, 1)])
+                        setSubscribedTargets(prev => {
+                            const next = { ...prev };
+                            delete next[subscriptionId];
+                            return next;
+                        });
                     },
                 }
-                return subscription
+                return subscription;
             },
         } as MenuProviderType
-    }, [])
+    }, []);
 
     return (
         <MenuContext.Provider value={value}>
diff --git a/react-ui/src/shared/provider/utils.provider.tsx b/react-ui/src/shared/provider/utils.provider.tsx
index ca6aa1d3235efe3e1f48441fc7a39a47e9ce7fb7..3a76bbe69430ca8f5031935f8e1d11925c620610 100644
--- a/react-ui/src/shared/provider/utils.provider.tsx
+++ b/react-ui/src/shared/provider/utils.provider.tsx
@@ -1,4 +1,4 @@
-import { BasicProp } from "@helper/interfaces";
+import { BasicProp } from "@shared/types/interfaces.type";
 import React, { createContext, useContext, useMemo } from "react";
 import { useTranslation } from "react-i18next";
 import { toast } from "react-toastify";
diff --git a/react-ui/src/shared/reducer/routine.reducer.ts b/react-ui/src/shared/reducer/routine.reducer.ts
index a52ee84d3f16e89f5e899919d59fcb44428a9213..95d5f06e7d708965ec65b2960f6f56823a976f6b 100755
--- a/react-ui/src/shared/reducer/routine.reducer.ts
+++ b/react-ui/src/shared/reducer/routine.reducer.ts
@@ -1,58 +1,42 @@
 import { PayloadAction, createSlice } from '@reduxjs/toolkit'
+import { CategoryType } from '@shared/types/category.type'
+import { ThunkDTO, ThunkPersist } from '@shared/types/thunk.type'
 import { RoutineManager } from '@utils/routine.manager'
 import { RootState } from '../../stores'
 import { startListening } from '../../stores/middleware/listener.middleware'
 import { setToken } from './user.reducer'
 
-interface ThunkEntityDTO {
-    thunk: any
-    payload: Object
-
-    /**
-     * Only one subscription per category is allowed. New subscription will unsubscribe and overwrite the old one
-     */
-    category: CATEGORIES
-}
-
-interface ThunkEntity extends ThunkEntityDTO {
-    id?: number
-    locked: boolean
-}
-
 export interface ReducerState {
-    thunks: { [key in keyof typeof CATEGORIES]: ThunkEntity | null }
-}
-
-export enum CATEGORIES {
-    TABLE,
-    TAB,
+    thunks: Record<CategoryType, ThunkPersist | null>
 }
 
 const initialState: ReducerState = {
     thunks: {
+        DEVICE: null,
         TABLE: null,
-        TAB: null,
+        TAB: null
     },
+
 }
 
+
 const RoutineSlice = createSlice({
     name: 'routine',
     initialState,
     reducers: {
-        addRoutine: (state: any, { payload }: PayloadAction<ThunkEntityDTO>) => {
-            const newThunk: ThunkEntity = { ...payload, locked: true }
-            state.thunks[CATEGORIES[payload.category]] = newThunk
-        },
-
-        setThunkId: (state, { payload }: PayloadAction<{ id: number; category: CATEGORIES }>) => {
-            const thunk = state.thunks[CATEGORIES[payload.category] as any]
-
-            if (!thunk) {
-                // TODO
-                throw new Error('Thunk not found')
+        addRoutine: (state: any, { payload }: PayloadAction<ThunkDTO>) => {
+            const thunk: ThunkPersist = {
+                category: payload.category,
+                payload: payload.payload,
+                thunkId: payload.thunk.id,
+                lastupdate: Date.now()
             }
 
-            state.thunks[CATEGORIES[payload.category] as any] = { ...thunk, id: payload.id, locked: false }
+            state.thunks[payload.category] = thunk
+        },
+
+        refreshUpdateTimer: (state: any, { payload }: PayloadAction<CategoryType>) => {
+            state.thunks[payload].lastupdate = Date.now()
         },
 
         removeAll: (state) => {
@@ -61,7 +45,7 @@ const RoutineSlice = createSlice({
     },
 })
 
-export const { addRoutine } = RoutineSlice.actions
+export const { addRoutine, refreshUpdateTimer } = RoutineSlice.actions
 
 // on logout remove all routine
 startListening({
@@ -72,49 +56,37 @@ startListening({
     },
 })
 
-// on rehydrate add all persistet routines
-// TODO -> thunk does not have the thunk function object due to its coming from the store that ignores the value.
-// at this point we have to figure out how to get the thunk function out of the "string" name
-// startListening({
-//     predicate: ({ type }) => type === REHYDRATE,
-//     effect: async (_, listenerApi) => {
-//         const { routine } = listenerApi.getState() as RootState
-//         for (const [_, thunk] of Object.entries<ThunkEntity>(routine.thunks)) {
-//             if (!thunk) {
-//                 continue
-//             }
-//             const dto: ThunkEntityDTO = thunk
-//             listenerApi.dispatch(addRoutine(dto))
-//         }
-//     },
-// })
-
-// add new routine
+/**
+ * Add new routine
+ * 
+ * This listener handles the connection between the RoutingManager that 
+ * stores the non persistable thunk object and the peristable thunk information.
+ * The persistable information are stored in this reducer
+ */
 startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
-        const { thunk } = action.payload as ThunkEntity
+        const { thunk } = action.payload as ThunkDTO
         const subscription = await listenerApi.dispatch(thunk(action.payload.payload))
-        const thunkId = await RoutineManager.add(subscription.payload)
-        listenerApi.dispatch(
-            RoutineSlice.actions.setThunkId({ id: thunkId, category: action.payload.category })
-        )
+
+        if (subscription.error) {
+            throw new Error('Error during routine execution: ' + subscription.error.message)
+        }
+
+        RoutineManager.add(subscription.payload, action.payload.category)
     },
 })
 
-// unsubscribe old routine
+// unsubscribe old routine that is in the same category
 startListening({
     predicate: (action) => addRoutine.match(action),
     effect: async (action, listenerApi) => {
         const { routine } = listenerApi.getOriginalState() as RootState
-        const lastThunk = routine.thunks[CATEGORIES[action.payload.category] as any]
-        if (lastThunk) {
-            if (!lastThunk.id) {
-                throw new Error()
-                // TODO
-            }
+        const category = action.payload.category;
 
-            RoutineManager.unsubscribe(lastThunk.id)
+        const lastThunk = routine.thunks[category as CategoryType]
+        if (lastThunk) {
+            RoutineManager.unsubscribe(category)
         }
     },
 })
diff --git a/react-ui/src/shared/reducer/user.reducer.ts b/react-ui/src/shared/reducer/user.reducer.ts
index af0f2d171675627a0a69f5aebd6e7b64125fba37..a0f2d422255f9a135236c4d8c2e3a70a5e51ec46 100755
--- a/react-ui/src/shared/reducer/user.reducer.ts
+++ b/react-ui/src/shared/reducer/user.reducer.ts
@@ -1,7 +1,6 @@
-import { api, RbacUser, UserServiceGetUsersApiArg } from '@api/api'
+import { RbacUser } from '@api/api'
 import { setCookieValue } from '@helper/coookie'
-import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
-import { RootState } from '..'
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
 
 export interface UserSliceState {
     // defined by the frontend user input. This value is getting compared with the backend response
@@ -34,27 +33,4 @@ export const { setToken } = userSlice.actions
 export const { setUser } = userSlice.actions
 
 export default userSlice.reducer
-export const userReducerPath = userSlice.reducerPath
-
-export const fetchUser = createAsyncThunk('user/fetchUser', (_, thunkAPI) => {
-    const payload: UserServiceGetUsersApiArg = {}
-
-    thunkAPI.dispatch(api.endpoints.userServiceGetUsers.initiate(payload)).then((response) => {
-        if (response.error || !response.data?.user?.length) {
-            // TODO proper error handling
-            throw new Error('Fetching the pnd list after successful login failed')
-        }
-
-        const { user } = thunkAPI.getState() as RootState
-
-        // TODO ask if this is the correct approach
-        const matchedUser = response.data.user.find((_user) => _user.name === user.username)
-
-        if (!matchedUser) {
-            // TODO proper error handling
-            throw new Error('No user found with the provided username')
-        }
-
-        thunkAPI.dispatch(setUser(matchedUser))
-    })
-})
+export const userReducerPath = userSlice.reducerPath
\ No newline at end of file
diff --git a/react-ui/src/shared/routine/user.routine.ts b/react-ui/src/shared/routine/user.routine.ts
new file mode 100644
index 0000000000000000000000000000000000000000..368ccf41246176c533b2394a581124200668f527
--- /dev/null
+++ b/react-ui/src/shared/routine/user.routine.ts
@@ -0,0 +1,37 @@
+import { api, PndServiceGetPndListApiArg, UserServiceGetUsersApiArg } from "@api/api"
+import { setPnds } from "@component/devices/reducer/device.reducer"
+import { createAsyncThunk } from "@reduxjs/toolkit"
+import { setUser } from "@shared/reducer/user.reducer"
+import { RootState } from "src/stores"
+
+export const fetchUser = createAsyncThunk('user/fetchUser', (_, thunkAPI) => {
+    const payload: UserServiceGetUsersApiArg = {}
+
+    thunkAPI.dispatch(api.endpoints.userServiceGetUsers.initiate(payload)).then((response) => {
+        if (response.error || !response.data?.user?.length) {
+            // TODO proper error handling
+            throw new Error('Fetching the pnd list after successful login failed')
+        }
+
+        const { user } = thunkAPI.getState() as RootState
+        const matchedUser = response.data.user.find((_user) => _user.name === user.username)
+
+        if (!matchedUser) {
+            // TODO proper error handling => logout
+            throw new Error('No user found with the provided username')
+        }
+
+        thunkAPI.dispatch(setUser(matchedUser))
+    })
+})
+
+export const fetchPnds = createAsyncThunk('device/fetchPnds', (_, thunkApi) => {
+    const payload: PndServiceGetPndListApiArg = {
+        timestamp: new Date().getTime().toString(),
+    }
+
+    const subscription = thunkApi.dispatch(api.endpoints.pndServiceGetPndList.initiate(payload))
+    subscription.unwrap().then((response) => {
+        thunkApi.dispatch(setPnds(response.pnd))
+    })
+})
\ No newline at end of file
diff --git a/react-ui/src/shared/style/box.scss b/react-ui/src/shared/style/box.scss
index bd75fb00a6c1574584c85afafe6a91d8b858deb4..53158f51586c931dc7855c8423b2d40f7371c2bc 100755
--- a/react-ui/src/shared/style/box.scss
+++ b/react-ui/src/shared/style/box.scss
@@ -1,26 +1,59 @@
-@import './colors.scss';
+@import "./colors.scss";
 
 $box-padding: 10px;
-$border-radius: 20px;
- 
+$border-radius: 0.25em;
+$border-width: 2px;
+$transition-duration: 0.3s;
 
 .c-box {
     padding: $box-padding;
     background-color: white;
-    box-shadow: 0px 4px 4px rgba(0,0,0, .35);
+    position: relative;
     border-radius: $border-radius;
+    transition: box-shadow $transition-duration ease-in-out;
+
+    background:
+        linear-gradient(white, white) padding-box,
+        linear-gradient(
+                180deg,
+                rgba(map-get($theme-colors, "primary"), 0.4) 0%,
+                rgba(map-get($theme-colors, "primary"), 0.2) 40%,
+                rgba(map-get($theme-colors, "primary"), 0.1) 100%
+            )
+            border-box;
+    border: $border-width solid transparent;
+    box-shadow: $box-shadow;
+
+    &::before {
+        content: "";
+        position: absolute;
+        top: -$border-width;
+        left: -$border-width;
+        right: -$border-width;
+        bottom: -$border-width;
+        background: linear-gradient(
+            180deg,
+            rgba(map-get($theme-colors, "primary"), 0.4) 0%,
+            rgba(map-get($theme-colors, "primary"), 0.2) 60%,
+            rgba(map-get($theme-colors, "primary"), 0.1) 100%
+        );
+        border-radius: inherit;
+        z-index: -1;
+        opacity: 0;
+        transition: opacity $transition-duration ease-in-out;
+    }
+
+    &:hover {
+        box-shadow: 0 0.5rem 1rem rgba(map-get($theme-colors, "primary"), 0.2);
+
+        &::before {
+            opacity: 1;
+        }
+    }
 }
 
 .abstract-box {
-    padding: 16px $box-padding;
-    font-size: .90em;
+    padding: $box-padding;
+    font-size: 0.9em;
     border-radius: calc($border-radius / 2);
 }
-
-
-// @each $color, $value in $theme-colors {
-//     .#{$color}-box {
-//         @extend .abstract-box;
-//         background-color: $value !important;
-//     }
-// }
diff --git a/react-ui/src/shared/style/colors.scss b/react-ui/src/shared/style/colors.scss
index 749af9e8eb8e86b0169c7f2a652dc0bada992181..4469a4a5b9fc0c32733bcb0afe112f568a5e5492 100755
--- a/react-ui/src/shared/style/colors.scss
+++ b/react-ui/src/shared/style/colors.scss
@@ -1,11 +1,13 @@
 $theme-colors: (
-  'primary': #b350e0,
-  'primary::hover': #ddaff3af,
-  'bg-primary': #E1E1E1,
-  'danger': #ffdcdc,
-  'warning': #dbd116,
-  'dark': #595959,
-  'black': #000000,
+  "primary": #b350e0,
+  "primary::hover": #ddaff3af,
+  "bg-primary": #ededed,
+  "danger": #ff0000,
+  "warning": #dbd116,
+  "dark": #595959,
+  "black": #000000
 );
-  
-@import '/node_modules/bootstrap/scss/bootstrap';
+
+$box-shadow: 0px 4px 8px rgba(map-get($theme-colors, "primary"), 0.2);
+
+@import "/node_modules/bootstrap/scss/bootstrap";
diff --git a/react-ui/src/shared/style/fonts.scss b/react-ui/src/shared/style/fonts.scss
index c47d1a52fb36da1863fc511e7e1e4deb045694cf..3af44e1559b21507bd509acb4c954a204250f3ba 100755
--- a/react-ui/src/shared/style/fonts.scss
+++ b/react-ui/src/shared/style/fonts.scss
@@ -1,9 +1,12 @@
 @font-face {
     font-family: Inter;
-    src: url("/fonts/Inter.ttf");
+    src:
+        url("/fonts/inter-webfont.woff2") format("woff2"),
+        url("/fonts/inter-webfont.woff") format("woff");
+    font-weight: normal;
+    font-style: normal;
 }
 
-
 * {
     font-family: Inter;
-}
\ No newline at end of file
+}
diff --git a/react-ui/src/shared/style/utils.scss b/react-ui/src/shared/style/utils.scss
index d8be654f7e67fc11d9626c2a52f5744af0dfbb79..d6f34d301416ac7aeb82aa740cd150e95efbd6c5 100755
--- a/react-ui/src/shared/style/utils.scss
+++ b/react-ui/src/shared/style/utils.scss
@@ -7,7 +7,3 @@
         cursor: pointer;
     }
 }
-
-.icon {
-    font-size: 1.75em;
-}
diff --git a/react-ui/src/shared/types/category.type.ts b/react-ui/src/shared/types/category.type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e08282341e803cf8b62bf0deec423fb0ebe62234
--- /dev/null
+++ b/react-ui/src/shared/types/category.type.ts
@@ -0,0 +1,17 @@
+
+const DeviceListView = {
+    TABLE: "device_list/table",
+    TAB: "device_list/tab",
+}
+
+const Shared = {
+    DEVICE: 'objects/device'
+}
+
+
+export const Category = {
+    ...DeviceListView,
+    ...Shared
+}
+
+export type CategoryType = keyof typeof Category
\ No newline at end of file
diff --git a/react-ui/src/shared/helper/interfaces.ts b/react-ui/src/shared/types/interfaces.type.ts
similarity index 100%
rename from react-ui/src/shared/helper/interfaces.ts
rename to react-ui/src/shared/types/interfaces.type.ts
diff --git a/react-ui/src/shared/types/thunk.type.ts b/react-ui/src/shared/types/thunk.type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ff037d79655b7596295b387c61de4755dcc2eff4
--- /dev/null
+++ b/react-ui/src/shared/types/thunk.type.ts
@@ -0,0 +1,24 @@
+import { CategoryType } from "./category.type"
+
+
+// The actual Thunk type is hard to determine because is very generic 
+export type ThunkFunc = any
+
+
+export interface ThunkDTO {
+    thunk: ThunkFunc
+    payload: Object
+
+    /**
+     * Only one subscription per category is allowed.
+     * New subscription will unsubscribe and overwrite the old one
+     */
+    category: CategoryType
+}
+
+export interface ThunkPersist {
+    thunkId: number,
+    payload: Object
+    category: CategoryType,
+    lastupdate: number
+}
diff --git a/react-ui/src/shared/utils/loading-fallback.tsx b/react-ui/src/shared/utils/loading-fallback.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d1c6daa4aa369ca157dbae653c6dd29f6f99d99a
--- /dev/null
+++ b/react-ui/src/shared/utils/loading-fallback.tsx
@@ -0,0 +1,117 @@
+import React, { useEffect, useState } from 'react';
+import { Col, Container, Row } from 'react-bootstrap';
+import logo from '/public/logo.png';
+
+interface DelayedRenderProps {
+    children: React.ReactNode;
+    loading?: {
+        minimumLoadingTime: number;
+        component: () => JSX.Element
+    }
+}
+
+export const SplashScreen = () => {
+    const [dots, setDots] = useState('');
+
+    useEffect(() => {
+        const dotsInterval = setInterval(() => {
+            setDots(prev => prev.length >= 3 ? '' : prev + '.');
+        }, 500);
+
+        return () => clearInterval(dotsInterval);
+    }, []);
+
+    return (
+        <div className="splash-screen-overlay">
+            <Container fluid className="h-100 d-flex align-items-center justify-content-center bg-bg-primary">
+                <Row>
+                    <Col className="text-center">
+                        <div className="loading-bounce mb-4">
+                            <img
+                                src={logo}
+                                alt="Logo"
+                                className="img-fluid"
+                                style={{ width: '120px', height: '120px', objectFit: 'contain' }}
+                            />
+                        </div>
+                        <div className="loading-text">
+                            <span className="h4 text-secondary">Loading</span>
+                            <span className="h4 text-secondary dots-width">{dots}</span>
+                        </div>
+                    </Col>
+                </Row>
+            </Container>
+
+            <style>
+                {`
+                    .splash-screen-overlay {
+                        position: fixed;
+                        top: 0;
+                        left: 0;
+                        width: 100%;
+                        height: 100%;
+                        background-color: #f8f9fa;
+                        z-index: 0;
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+                    }
+
+                    .loading-bounce {
+                        animation: bounce 1s infinite;
+                    }
+
+                    @keyframes bounce {
+                        0%, 100% {
+                            transform: translateY(0);
+                        }
+                        50% {
+                            transform: translateY(-20px);
+                        }
+                    }
+
+                    .loading-text {
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                    }
+
+                    .dots-width {
+                        min-width: 24px;
+                        text-align: left;
+                        margin-left: 2px;
+                    }
+                `}
+            </style>
+        </div>
+    );
+};
+
+export const DelayedRender: React.FC<DelayedRenderProps> = ({
+    children,
+    loading
+}) => {
+    const [shouldRender, setShouldRender] = useState(false);
+
+    useEffect(() => {
+        if (!loading) {
+            setShouldRender(true);
+            return;
+        }
+
+        const timer = setTimeout(() => {
+            setShouldRender(true);
+        }, loading.minimumLoadingTime);
+
+        return () => clearTimeout(timer);
+    }, [loading]);
+
+    if (!shouldRender && loading) {
+        const LoadingComponent = loading.component;
+        return <LoadingComponent />;
+    }
+
+    return <>{children}</>;
+};
+
+export default DelayedRender;
\ No newline at end of file
diff --git a/react-ui/src/shared/utils/routine.manager.ts b/react-ui/src/shared/utils/routine.manager.ts
index ade07928976f5be714475325fceb2003ab6e75f0..1fee6723e7f36531f330160aadfc77d60ebfce82 100755
--- a/react-ui/src/shared/utils/routine.manager.ts
+++ b/react-ui/src/shared/utils/routine.manager.ts
@@ -1,67 +1,75 @@
+import { infoMessage, warnMessage } from '@helper/debug'
 import { QueryActionCreatorResult } from '@reduxjs/toolkit/query'
+import { Category, CategoryType } from '@shared/types/category.type'
 
 type Routine = QueryActionCreatorResult<any>
 
 interface Entity {
     routine: Routine
-    id: number
 }
 
-const initialState = {
-    routines: [] as Entity[],
+
+interface RoutineState {
+    routines: Record<CategoryType, Entity | null>
+}
+
+const initalState: RoutineState = {
+    routines: {
+        DEVICE: null,
+        TABLE: null,
+        TAB: null
+    }
 }
 
 /**
- * Routine manager is a singleton that holds all running routines.
- * The redux store holds any persistable information about the routines.
- * The routines objects itself are stored in the RoutineManager.
- */
+* Routine manager is a singleton that holds all running routines.
+* The redux store holds any persistable information about the routines.
+* The routine objects itself are stored in the RoutineManager.
+*/
 export const RoutineManager = (() => {
-    const state = initialState
-    const add = (routine: Routine): number => {
-        const id = state.routines.length
+    let state = initalState
 
-        const newEntity: Entity = {
+    const add = (routine: Routine, category: CategoryType): boolean => {
+        const entity: Entity = {
             routine: routine,
-            id,
         }
 
-        state.routines = [...state.routines, newEntity]
+        state.routines = {
+            ...state.routines,
+            [category]: entity
+        }
+        infoMessage("Routine subscribed to category " + category)
 
-        return id
+        return true
     }
 
     const unsubscribeAll = () => {
-        state.routines.forEach(({ routine: subscription }) => {
-            _unsubscribe(subscription)
-        })
+        Object.keys(state.routines)
+            .forEach((category) => {
+                unsubscribe(category as CategoryType)
+            })
 
-        state.routines = initialState.routines
+        state = initalState
     }
 
     /**
      * @param id
      * @returns returns true if the routine was stopped, false if it was not found
      */
-    const unsubscribe = (id: number): boolean => {
-        const routine = state.routines.find(({ id: routineId }) => routineId === id)
+    const unsubscribe = (category: CategoryType): boolean => {
+        const entity = state.routines[category]
 
-        if (routine) {
-            _unsubscribe(routine.routine)
+        if (entity) {
+            entity.routine.unsubscribe()
+            state.routines[category] = null
+            infoMessage("Routine unsubscribed from category " + category)
         }
 
-        return !!routine
-    }
+        if (!!entity) {
+            warnMessage("Desired routine to unsubscribe does not exist in category " + Category[category])
+        }
 
-    /**
-     * Actual unsubscribe process.
-     * This process is extracted to have a single process of unsubscribing.
-     *
-     * @param subscription
-     */
-    const _unsubscribe = (subscription: Routine) => {
-        subscription.unsubscribe()
-        // TODO remove from state
+        return !!entity
     }
 
     return {
@@ -69,4 +77,4 @@ export const RoutineManager = (() => {
         unsubscribe,
         unsubscribeAll,
     }
-})()
+})()
\ No newline at end of file
diff --git a/react-ui/tsconfig.json b/react-ui/tsconfig.json
index c679b7100669ddf234324cecde3dd37ab94c95d7..d40af31cce3f086a5b0b5c6870cfad85ce4d50a8 100755
--- a/react-ui/tsconfig.json
+++ b/react-ui/tsconfig.json
@@ -22,7 +22,6 @@
 
         "baseUrl": ".",
         "paths": {
-            "@assets/*": ["assets/*"],
             "@api/*": ["src/shared/api/*"],
             "@reducer/*": ["src/stores/reducer/*"],
             "@provider/*": ["src/shared/provider/*"],
diff --git a/react-ui/vite.config.mjs b/react-ui/vite.config.mjs
index fefafe05a692ebf39a8879ac440fcff0f549ec60..33fd69cc76fd094e53f168c6babdf0da367d897c 100755
--- a/react-ui/vite.config.mjs
+++ b/react-ui/vite.config.mjs
@@ -1,17 +1,39 @@
-import react from '@vitejs/plugin-react'
-import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
 
 export default defineConfig({
     plugins: [react()],
     build: {
-        sourcemap: true,
+        sourcemap: false,
+
+        rollupOptions: {
+            output: {
+                manualChunks: {
+                    'required': [
+                        'bootstrap', 'react-bootstrap',
+                        'react', 'react-dom', 'react-router-dom',
+                        'redux', '@reduxjs/toolkit', 'react-redux', 'redux-observable', 'redux-persist',
+                        'i18next', 'react-i18next',
+                        '@fortawesome/fontawesome-svg-core',
+                        '@fortawesome/free-regular-svg-icons',
+                        '@fortawesome/free-solid-svg-icons',
+                        '@fortawesome/react-fontawesome'
+                    ],
+                    'lazy': [
+                        'react-toastify'
+                    ]
+                }
+            }
+        }
     },
     // develop server
     server: {
+        sourcemap: true,
+        host: true,
         port: 3000,
         proxy: {
             '/api': {
-                target: 'http://127.0.0.1:8080',
+                target: 'http://clab-gosdn_csbi_arista_base-gosdn:8080',
                 changeOrigin: true,
                 secure: false,
                 rewrite: (path) => path.replace(/^\/api/, ''),
@@ -35,7 +57,6 @@ export default defineConfig({
     },
     resolve: {
         alias: {
-            '@assets': '/assets',
             '@api': '/src/shared/api',
             '@reducer': '/src/stores/reducer',
             '@provider': '/src/shared/provider',
diff --git a/react-ui/yarn.lock b/react-ui/yarn.lock
index d054db6d6ed77067217fd0ca327f855376706356..db8fb7d3c16b6ee32f476f99416a6a89236a6891 100755
--- a/react-ui/yarn.lock
+++ b/react-ui/yarn.lock
@@ -1486,6 +1486,13 @@
   dependencies:
     prop-types "^15.8.1"
 
+"@fullhuman/postcss-purgecss@^7.0.2":
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-7.0.2.tgz#ccacdbc312248c76c42cfac359f4ca5121001e67"
+  integrity sha512-U4zAXNaVztbDxO9EdcLp51F3UxxYsb/7DN89rFxFJhfk2Wua2pvw2Kf3HdspbPhW/wpHjSjsxWYoIlbTgRSjbQ==
+  dependencies:
+    purgecss "^7.0.2"
+
 "@humanfs/core@^0.19.1":
   version "0.19.1"
   resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
@@ -2586,6 +2593,13 @@
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.3.tgz#3654138d0da1b0c7916f6ed0dc1cc2b576d47650"
   integrity sha512-uTYkxTLkYp41nq/ULXyXMtkNT1vu5fXJoqad6uTNCOGat5t9cLgF4vMNLBXsTOXpdOI44XzKPY1M5RRm0bQHuw==
 
+"@types/react-grid-layout@^1.3.5":
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz#f4b52bf27775290ee0523214be0987be14e66823"
+  integrity sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-transition-group@^4.4.6":
   version "4.4.11"
   resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5"
@@ -3836,7 +3850,12 @@ cliui@^8.0.1:
     strip-ansi "^6.0.1"
     wrap-ansi "^7.0.0"
 
-clsx@^2.1.0:
+clsx@^1.1.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+  integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
+clsx@^2.0.0, clsx@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
   integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -3906,6 +3925,11 @@ combined-stream@^1.0.8:
   dependencies:
     delayed-stream "~1.0.0"
 
+commander@^12.1.0:
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
+  integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
+
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -4061,6 +4085,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+crypto-js@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
+  integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
+
 crypto-random-string@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
@@ -5334,6 +5363,11 @@ fast-diff@^1.1.2:
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
   integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
 
+fast-equals@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7"
+  integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==
+
 fast-glob@^3.2.9, fast-glob@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@@ -5690,6 +5724,18 @@ glob@^10.3.10:
     package-json-from-dist "^1.0.0"
     path-scurry "^1.11.1"
 
+glob@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.0.tgz#6031df0d7b65eaa1ccb9b29b5ced16cea658e77e"
+  integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==
+  dependencies:
+    foreground-child "^3.1.0"
+    jackspeak "^4.0.1"
+    minimatch "^10.0.0"
+    minipass "^7.1.2"
+    package-json-from-dist "^1.0.0"
+    path-scurry "^2.0.0"
+
 glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -6469,6 +6515,13 @@ jackspeak@^3.1.2:
   optionalDependencies:
     "@pkgjs/parseargs" "^0.11.0"
 
+jackspeak@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.0.2.tgz#11f9468a3730c6ff6f56823a820d7e3be9bef015"
+  integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==
+  dependencies:
+    "@isaacs/cliui" "^8.0.2"
+
 jake@^10.8.5:
   version "10.9.2"
   resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f"
@@ -7277,6 +7330,11 @@ lru-cache@^10.2.0:
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
   integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
 
+lru-cache@^11.0.0:
+  version "11.0.2"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39"
+  integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==
+
 lru-cache@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -7407,6 +7465,13 @@ minimalistic-assert@^1.0.0:
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
   integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
 
+minimatch@^10.0.0:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b"
+  integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==
+  dependencies:
+    brace-expansion "^2.0.1"
+
 minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -7923,6 +7988,14 @@ path-scurry@^1.11.1:
     lru-cache "^10.2.0"
     minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
 
+path-scurry@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580"
+  integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==
+  dependencies:
+    lru-cache "^11.0.0"
+    minipass "^7.1.2"
+
 path-to-regexp@0.1.12:
   version "0.1.12"
   resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
@@ -8624,7 +8697,7 @@ prop-types-extra@^1.1.0:
     react-is "^16.3.2"
     warning "^4.0.0"
 
-prop-types@^15.6.2, prop-types@^15.8.1:
+prop-types@15.x, prop-types@^15.6.2, prop-types@^15.8.1:
   version "15.8.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -8653,6 +8726,16 @@ punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
   integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
 
+purgecss@^7.0.2:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-7.0.2.tgz#b7dccc3ead65a4301eed98e014793719a511c633"
+  integrity sha512-4Ku8KoxNhOWi9X1XJ73XY5fv+I+hhTRedKpGs/2gaBKU8ijUiIKF/uyyIyh7Wo713bELSICF5/NswjcuOqYouQ==
+  dependencies:
+    commander "^12.1.0"
+    glob "^11.0.0"
+    postcss "^8.4.47"
+    postcss-selector-parser "^6.1.2"
+
 q@^1.1.2:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@@ -8772,6 +8855,14 @@ react-dom@^18.3.1:
     loose-envify "^1.1.0"
     scheduler "^0.23.2"
 
+react-draggable@^4.0.3, react-draggable@^4.4.5:
+  version "4.4.6"
+  resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e"
+  integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
+  dependencies:
+    clsx "^1.1.1"
+    prop-types "^15.8.1"
+
 react-error-boundary@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.1.2.tgz#bc750ad962edb8b135d6ae922c046051eb58f289"
@@ -8784,6 +8875,18 @@ react-error-overlay@^6.0.11:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
   integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
 
+react-grid-layout@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.5.0.tgz#b6cc9412b58cf8226aebc0df7673d6fa782bdee2"
+  integrity sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==
+  dependencies:
+    clsx "^2.0.0"
+    fast-equals "^4.0.3"
+    prop-types "^15.8.1"
+    react-draggable "^4.4.5"
+    react-resizable "^3.0.5"
+    resize-observer-polyfill "^1.5.1"
+
 react-i18next@^15.0.0:
   version "15.1.4"
   resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.1.4.tgz#65c03c31a5e42202000652e163f22f23a9306a60"
@@ -8830,6 +8933,14 @@ react-refresh@^0.14.2:
   resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
   integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
 
+react-resizable@^3.0.5:
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1"
+  integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==
+  dependencies:
+    prop-types "15.x"
+    react-draggable "^4.0.3"
+
 react-router-dom@^6.23.1:
   version "6.28.0"
   resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.28.0.tgz#f73ebb3490e59ac9f299377062ad1d10a9f579e6"
@@ -9123,6 +9234,11 @@ reselect@^5.1.0:
   resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e"
   integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 resolve-cwd@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"