diff --git a/.gitignore b/.gitignore
index e0c919d..a2fb4eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ notes.txt
 toolbox/
 signing/
 frontend/
+.vscode/
diff --git a/go.mod b/go.mod
index 66cf4d0..c27016d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.22
 require (
 	github.com/fsnotify/fsnotify v1.8.0
 	github.com/fxamacker/cbor/v2 v2.5.0
+	github.com/gdamore/tcell/v2 v2.8.1
 	github.com/go-playground/validator/v10 v10.10.1
 	github.com/google/uuid v1.3.0
 	github.com/jinzhu/copier v0.4.0
@@ -15,9 +16,10 @@ require (
 	github.com/nats-io/nkeys v0.4.7
 	github.com/pkg/profile v1.7.0
 	github.com/prometheus/client_golang v1.14.0
+	github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57
 	github.com/tenebris-tech/tail v1.0.5
 	go.etcd.io/bbolt v1.3.7
-	golang.org/x/crypto v0.18.0
+	golang.org/x/crypto v0.23.0
 	golang.org/x/exp v0.0.0-20230321023759-10a507213a29
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -26,11 +28,14 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/felixge/fgprof v0.9.3 // indirect
+	github.com/gdamore/encoding v1.0.1 // indirect
 	github.com/go-playground/locales v0.14.0 // indirect
 	github.com/go-playground/universal-translator v0.18.0 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect
 	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/minio/highwayhash v1.0.2 // indirect
 	github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a // indirect
@@ -38,9 +43,11 @@ require (
 	github.com/prometheus/client_model v0.3.0 // indirect
 	github.com/prometheus/common v0.42.0 // indirect
 	github.com/prometheus/procfs v0.9.0 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
-	golang.org/x/sys v0.27.0 // indirect
-	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/sys v0.29.0 // indirect
+	golang.org/x/term v0.28.0 // indirect
+	golang.org/x/text v0.21.0 // indirect
 	golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
 	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
diff --git a/go.sum b/go.sum
index 96f54eb..3790e1d 100644
--- a/go.sum
+++ b/go.sum
@@ -15,6 +15,10 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/
 github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
 github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
+github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
+github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
+github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
 github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
@@ -29,8 +33,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
 github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
 github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
@@ -53,6 +57,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
 github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
@@ -81,6 +89,12 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI
 github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
 github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
 github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
+github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57 h1:LmsF7Fk5jyEDhJk0fYIqdWNuTxSyid2W42A0L2YWjGE=
+github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
@@ -96,36 +110,87 @@ github.com/tenebris-tech/tail v1.0.5 h1:gKDA1qEP+kxG/SqaFzJWC/5jLHlG1y4hgibSPHdi
 github.com/tenebris-tech/tail v1.0.5/go.mod h1:RpxaZO+UNwbKgXA6VzHdR5FTLTM0pk9zZU/mmHxUeZQ=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
 go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
 golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-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.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.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/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
 golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
diff --git a/message_and_subject.go b/message_and_subject.go
index c9261bd..450defa 100644
--- a/message_and_subject.go
+++ b/message_and_subject.go
@@ -89,6 +89,12 @@ type Message struct {
 	Schedule []int `json:"schedule" yaml:"schedule"`
 	// Use auto detection of shell for cliCommands
 	UseDetectedShell bool `json:"useDetectedShell" yaml:"useDetectedShell"`
+	// MethodInstructions is a string that contains eventual extra instructions
+	// for the method that is used when the method is executed.
+	MethodInstructions []string `json:"methodInstructions" yaml:"methodInstructions"`
+	// ReplyMethodInstructions is a string that contains eventual extra instructions
+	// for the method that is used when the reply method is executed for the reply message.
+	ReplyMethodInstructions []string `json:"replyMethodInstructions" yaml:"replyMethodInstructions"`
 }
 
 // --- Subject
diff --git a/message_readers.go b/message_readers.go
index 8d6bd10..495612f 100644
--- a/message_readers.go
+++ b/message_readers.go
@@ -543,6 +543,9 @@ func (s *server) readHttpListener() {
 		}
 		mux := http.NewServeMux()
 		mux.HandleFunc("/", s.readHTTPlistenerHandler)
+		// TODO: Make this configurable, and move it out of the readHttpListener function,
+		// make a separate conf flag and function for it.
+		mux.Handle("/webui/", http.StripPrefix("/webui/", http.FileServer(http.Dir("/Users/bt/ctrl/webui"))))
 
 		err = http.Serve(n, mux)
 		if err != nil {
diff --git a/requests.go b/requests.go
index de9cb63..a08fa8f 100644
--- a/requests.go
+++ b/requests.go
@@ -169,6 +169,9 @@ const (
 	AclExport = "aclExport"
 	// REQAclImport
 	AclImport = "aclImport"
+
+	// WebUI is used to send messages to the web ui.
+	WebUI Method = "webUI"
 )
 
 type Handler func(proc process, message Message, node string) ([]byte, error)
@@ -230,6 +233,8 @@ func (m Method) GetMethodsAvailable() MethodsAvailable {
 			AclExport:                     Handler(methodAclExport),
 			AclImport:                     Handler(methodAclImport),
 			Test:                          Handler(methodTest),
+
+			WebUI: Handler(nil),
 		},
 	}
 
@@ -353,17 +358,18 @@ func newReplyMessage(proc process, message Message, outData []byte) {
 		// are injected f.ex. on a socket, and there they are directly converted into separate
 		// node messages. With other words a message in the system are only for single nodes,
 		// so we don't have to worry about the ToNodes field when creating reply messages.
-		FromNode:      message.ToNode,
-		Data:          outData,
-		Method:        message.ReplyMethod,
-		MethodArgs:    message.ReplyMethodArgs,
-		MethodTimeout: message.ReplyMethodTimeout,
-		IsReply:       true,
-		RetryWait:     message.RetryWait,
-		ACKTimeout:    message.ReplyACKTimeout,
-		Retries:       message.ReplyRetries,
-		Directory:     message.Directory,
-		FileName:      message.FileName,
+		FromNode:           message.ToNode,
+		Data:               outData,
+		Method:             message.ReplyMethod,
+		MethodArgs:         message.ReplyMethodArgs,
+		MethodTimeout:      message.ReplyMethodTimeout,
+		IsReply:            true,
+		RetryWait:          message.RetryWait,
+		ACKTimeout:         message.ReplyACKTimeout,
+		Retries:            message.ReplyRetries,
+		Directory:          message.Directory,
+		FileName:           message.FileName,
+		MethodInstructions: message.ReplyMethodInstructions,
 
 		// Put in a copy of the initial request message, so we can use it's properties if
 		// needed to for example create the file structure naming on the subscriber.
diff --git a/server.go b/server.go
index ae23a93..c53c945 100644
--- a/server.go
+++ b/server.go
@@ -631,6 +631,16 @@ func (s *server) exposeDataFolder() {
 // messageSerializeAndCompress will serialize and compress the Message, and
 // return the result as a []byte.
 func (s *server) messageSerializeAndCompress(msg Message) ([]byte, error) {
+	// NB: Implementing json encoding for WebUI messages for now.
+	if msg.Method == WebUI {
+		bSerialized, err := json.Marshal(msg)
+		if err != nil {
+			er := fmt.Errorf("error: messageDeliverNats: json encode message failed: %v", err)
+			return nil, er
+		}
+		fmt.Printf("JSON JSON JSON JSON JSON JSON JSON JSON JSON JSON JSON JSON \n")
+		return bSerialized, nil
+	}
 
 	// encode the message structure into cbor
 	bSerialized, err := cbor.Marshal(msg)
@@ -657,6 +667,7 @@ func (s *server) messageDeserializeAndUncompress(msgData []byte) (Message, error
 	// 	er := fmt.Errorf("info: subscriberHandlerJetstream: nats message received from %v, with subject %v ", headerFromNode, msg.Subject())
 	// 	s.errorKernel.logDebug(er)
 	// }
+	msgData2 := msgData
 
 	zr, err := zstd.NewReader(nil)
 	if err != nil {
@@ -665,9 +676,19 @@ func (s *server) messageDeserializeAndUncompress(msgData []byte) (Message, error
 	}
 	msgData, err = zr.DecodeAll(msgData, nil)
 	if err != nil {
-		er := fmt.Errorf("error: subscriberHandlerJetstream: zstd decoding failed: %v", err)
+		// er := fmt.Errorf("error: subscriberHandlerJetstream: zstd decoding failed: %v", err)
 		zr.Close()
-		return Message{}, er
+
+		// Not zstd encoded, try to decode as JSON. This is for messages from the WebUI.
+		var msg Message
+		fmt.Printf("DEBUG: msgData2: %v\n", string(msgData2))
+		err = json.Unmarshal(msgData2, &msg)
+		if err != nil {
+			return Message{}, err
+		}
+
+		// JSON decoded, return the message
+		return msg, nil
 	}
 
 	zr.Close()
diff --git a/tui/files/script.sh b/tui/files/script.sh
new file mode 100644
index 0000000..f748bdd
--- /dev/null
+++ b/tui/files/script.sh
@@ -0,0 +1 @@
+pwd
diff --git a/tui/files/script2.sh b/tui/files/script2.sh
new file mode 100644
index 0000000..f748bdd
--- /dev/null
+++ b/tui/files/script2.sh
@@ -0,0 +1 @@
+pwd
diff --git a/tui/files/script3.sh b/tui/files/script3.sh
new file mode 100644
index 0000000..f748bdd
--- /dev/null
+++ b/tui/files/script3.sh
@@ -0,0 +1 @@
+pwd
diff --git a/tui/main.go b/tui/main.go
new file mode 100644
index 0000000..ba7edca
--- /dev/null
+++ b/tui/main.go
@@ -0,0 +1,397 @@
+package main
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+
+	tcell "github.com/gdamore/tcell/v2"
+	"github.com/nats-io/nats.go"
+	"github.com/rivo/tview"
+)
+
+type CommandData struct {
+	ToNodes          []string `json:"toNodes"`
+	ToNode           string   `json:"toNode"`
+	JetstreamToNode  string   `json:"jetstreamToNode,omitempty"`
+	UseDetectedShell bool     `json:"useDetectedShell"`
+	Method           string   `json:"method"`
+	MethodArgs       []string `json:"methodArgs"`
+	MethodTimeout    int      `json:"methodTimeout"`
+	FromNode         string   `json:"fromNode"`
+	ReplyMethod      string   `json:"replyMethod"`
+	ACKTimeout       int      `json:"ACKTimeout"`
+}
+
+// Create a global variable for the output view so it can be accessed from NATS handlers
+var outputView *tview.TextView
+var historyList *tview.List
+var filesList *tview.List
+var app *tview.Application
+var nc *nats.Conn
+
+// Store form values globally
+var (
+	toNodes    string = "btdev1"
+	methodArgs        = []string{"/bin/bash", "-c", "ls -l"}
+	// Store history commands
+	historyCommands []string
+)
+
+func createCommandPage() tview.Primitive {
+	// Create a flex container for the entire page
+	mainFlex := tview.NewFlex().SetDirection(tview.FlexRow)
+
+	// Create form for inputs
+	formFields := tview.NewForm()
+	formFields.SetBorder(true).SetTitle("Command Form")
+
+	// Create a List for command history
+	historyList = tview.NewList()
+	historyList.SetBorder(true)
+	historyList.SetTitle("History")
+	// Handle selection from history
+	historyList.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
+		// Update Method Arg 3 field with selected command
+		methodArg3Field := formFields.GetFormItemByLabel("Method Arg 3").(*tview.InputField)
+		methodArg3Field.SetText(mainText)
+		methodArgs[2] = mainText // Update global variable
+		app.SetFocus(formFields) // Return focus to form
+	})
+
+	// Create a List for files
+	filesList = tview.NewList()
+	filesList.SetBorder(true)
+	filesList.SetTitle("Files")
+	filesList.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
+		// When a file is selected, update Method Arg 3 with the CTRL_FILE template
+		methodArg3Field := formFields.GetFormItemByLabel("Method Arg 3").(*tview.InputField)
+		methodArg3Field.SetText("{{CTRL_FILE:" + secondaryText + "}}")
+		methodArgs[2] = methodArg3Field.GetText()
+		app.SetFocus(formFields)
+	})
+
+	// Create a vertical flex for history and files lists
+	rightPanelFlex := tview.NewFlex().
+		SetDirection(tview.FlexRow).
+		AddItem(historyList, 0, 1, false).
+		AddItem(filesList, 0, 1, false)
+
+	// Create a horizontal flex for the form and right panel
+	topFlex := tview.NewFlex().
+		AddItem(formFields, 0, 2, true).
+		AddItem(rightPanelFlex, 0, 1, false)
+
+	// Create output text view
+	outputView = tview.NewTextView().
+		SetDynamicColors(true).
+		SetChangedFunc(func() {
+			app.Draw()
+		})
+	outputView.SetBorder(true).SetTitle("Output")
+
+	// Add form fields
+	formFields.AddInputField("To Nodes", toNodes, 30, nil, func(text string) {
+	})
+
+	formFields.AddInputField("Jetstream To Node", "", 30, nil, func(text string) {
+		// Value will be read directly from form when needed
+	})
+
+	formFields.AddCheckbox("Use Detected Shell", false, func(checked bool) {
+		// Value will be read directly from form when needed
+	})
+
+	formFields.AddInputField("Method", "cliCommand", 30, nil, func(text string) {
+		// Value will be read directly from form when needed
+	})
+
+	formFields.AddInputField("Method Arg 1", methodArgs[0], 30, nil, func(text string) {
+		methodArgs[0] = text
+	})
+
+	formFields.AddInputField("Method Arg 2", methodArgs[1], 30, nil, func(text string) {
+		methodArgs[1] = text
+	})
+
+	formFields.AddInputField("Method Arg 3", methodArgs[2], 30, nil, func(text string) {
+		methodArgs[2] = text
+	}).SetFieldStyle(tcell.StyleDefault.Blink(true))
+
+	formFields.AddInputField("Method Timeout", "3", 30, nil, func(text string) {
+		// Value will be read directly from form when needed
+	})
+
+	formFields.AddInputField("Reply Method", "webUI", 30, nil, func(text string) {
+		// Value will be read directly from form when needed
+	})
+
+	formFields.AddInputField("ACK Timeout", "0", 30, nil, func(text string) {
+		// Value will be read directly from form when needed
+	})
+
+	// Subscribe to responses
+	fromNode := "btdev1"
+
+	// Add buttons
+	formFields.AddButton("Send", func() {
+		// Get and save the current command to history
+		currentCmd := formFields.GetFormItemByLabel("Method Arg 3").(*tview.InputField).GetText()
+		if currentCmd != "" {
+			// Add to history if not already present
+			found := false
+			for _, cmd := range historyCommands {
+				if cmd == currentCmd {
+					found = true
+					break
+				}
+			}
+			if !found {
+				historyCommands = append([]string{currentCmd}, historyCommands...)
+				historyList.Clear()
+				for _, cmd := range historyCommands {
+					historyList.AddItem(cmd, "", 0, nil).ShowSecondaryText(false)
+				}
+			}
+		}
+
+		// Get current values from form
+		methodTimeout, _ := strconv.Atoi(formFields.GetFormItemByLabel("Method Timeout").(*tview.InputField).GetText())
+		ackTimeout, _ := strconv.Atoi(formFields.GetFormItemByLabel("ACK Timeout").(*tview.InputField).GetText())
+
+		// Get and clean toNodes
+		toNodesText := formFields.GetFormItemByLabel("To Nodes").(*tview.InputField).GetText()
+		toNodes := strings.Split(toNodesText, ",")
+		// Trim spaces and filter out empty nodes
+		// cleanedToNodes := []string{}
+		// for _, node := range toNodes {
+		// 	if trimmed := strings.TrimSpace(node); trimmed != "" {
+		// 		cleanedToNodes = append(cleanedToNodes, trimmed)
+		// 	}
+		// }
+
+		cmd := CommandData{
+			// ToNodes: The values of toNodes will be used to fill toNode field when sending below.
+			JetstreamToNode:  formFields.GetFormItemByLabel("Jetstream To Node").(*tview.InputField).GetText(),
+			UseDetectedShell: formFields.GetFormItemByLabel("Use Detected Shell").(*tview.Checkbox).IsChecked(),
+			Method:           formFields.GetFormItemByLabel("Method").(*tview.InputField).GetText(),
+			MethodArgs:       methodArgs,
+			MethodTimeout:    methodTimeout,
+			FromNode:         fromNode,
+			ReplyMethod:      formFields.GetFormItemByLabel("Reply Method").(*tview.InputField).GetText(),
+			ACKTimeout:       ackTimeout,
+		}
+
+		// Send command to each node
+		for _, node := range toNodes {
+			cmd.ToNode = node
+
+			// ---------------------------
+
+			var filePathToOpen string
+			foundFile := false
+
+			if strings.Contains(cmd.MethodArgs[2], "{{CTRL_FILE:") {
+				foundFile = true
+
+				// Example to split:
+				// echo {{CTRL_FILE:/somedir/msg_file.yaml}}>ctrlfile.txt
+				//
+				// Split at colon. We want the part after.
+				ss := strings.Split(cmd.MethodArgs[2], ":")
+				// Split at "}}",so pos [0] in the result contains just the file path.
+				sss := strings.Split(ss[1], "}}")
+				filePathToOpen = sss[0]
+
+			}
+
+			if foundFile {
+
+				fh, err := os.Open(filePathToOpen)
+				if err != nil {
+					fmt.Fprintf(outputView, "Error opening file: %v\n", err)
+					return
+				}
+				defer fh.Close()
+
+				b, err := io.ReadAll(fh)
+				if err != nil {
+					fmt.Fprintf(outputView, "Error reading file: %v\n", err)
+					return
+				}
+
+				// Replace the {{CTRL_FILE}} with the actual content read from file.
+				re := regexp.MustCompile(`(.*)({{CTRL_FILE.*}})(.*)`)
+				cmd.MethodArgs[2] = re.ReplaceAllString(cmd.MethodArgs[2], `${1}`+string(b)+`${3}`)
+
+				fmt.Fprintf(outputView, "DEBUG: Replaced {{CTRL_FILE}} with file content: %s\n", cmd.MethodArgs[2])
+				// ---
+
+			}
+
+			// ---------------------------
+
+			jsonData, err := json.Marshal(cmd)
+			if err != nil {
+				fmt.Fprintf(outputView, "Error creating command: %v\n", err)
+				return
+			}
+
+			subject := fmt.Sprintf("%s.%s", strings.TrimSpace(node), cmd.Method)
+
+			if err := nc.Publish(subject, jsonData); err != nil {
+				fmt.Fprintf(outputView, "Error sending to %s: %v\n", node, err)
+			} else {
+				// fmt.Fprintf(outputView, "*** Command %s sent to %s, subject: %s\n", jsonData, node, subject)
+			}
+		}
+	})
+
+	// Layout setup - adjust the proportion between form and output
+	mainFlex.AddItem(topFlex, 0, 7, true)     // Form and history section
+	mainFlex.AddItem(outputView, 0, 5, false) // Output takes remaining space
+
+	return mainFlex
+}
+
+func createNodesPage() tview.Primitive {
+	// Create an empty page for now
+	return tview.NewBox().SetBorder(true).SetTitle("Nodes")
+}
+
+func updateFilesList() {
+	// Clear current list
+	// filesList.Clear()
+
+	// Create tui/files directory if it doesn't exist
+	filesDir := "files"
+
+	// Read all files from the directory
+	files, err := os.ReadDir(filesDir)
+	if err != nil {
+		fmt.Fprintf(outputView, "Error reading files directory: %v\n", err)
+		return
+	}
+
+	if filesList == nil {
+		fmt.Printf("FILES FILE FILE: %v\n", files)
+		os.Exit(1)
+	}
+
+	// Add each file to the list
+	for _, file := range files {
+		//fmt.Fprintf(outputView, "DEBUG: File: %s\n", file.Name())
+		//if !file.IsDir() {
+		filesList.AddItem(file.Name(), filepath.Join(filesDir, file.Name()), 0, nil)
+		//}
+	}
+}
+
+func main() {
+	app = tview.NewApplication()
+
+	// Connect to NATS
+	var err error
+	nc, err = nats.Connect("nats://localhost:4222")
+	if err != nil {
+		panic(err)
+	}
+	defer nc.Close()
+
+	fromNode := "btdev1"
+	// --------------------------
+	// fmt.Fprintf(outputView, "DEBUG: BEFORE SUBSCRIBE\n")
+	sub, err := nc.Subscribe(fromNode+".webUI", func(msg *nats.Msg) {
+		var jsonMsg struct {
+			Data string `json:"data"`
+		}
+		if err := json.Unmarshal(msg.Data, &jsonMsg); err != nil {
+			app.QueueUpdateDraw(func() {
+				fmt.Fprintf(outputView, "Error decoding message: %v\n", err)
+			})
+			return
+		}
+
+		// Decode base64 data
+		decoded, err := base64.StdEncoding.DecodeString(jsonMsg.Data)
+		if err != nil {
+			app.QueueUpdateDraw(func() {
+				fmt.Fprintf(outputView, "Error decoding base64: %v\n", err)
+			})
+			return
+		}
+
+		app.QueueUpdateDraw(func() {
+			fmt.Fprintf(outputView, "\nReceived message on %s:\n%s\n", msg.Subject, string(decoded))
+		})
+	})
+	// fmt.Fprintf(outputView, "DEBUG: AFTER SUBSCRIBE\n")
+	if err != nil {
+		panic(err)
+	}
+	defer func() {
+		sub.Unsubscribe()
+	}()
+	// --------------------------
+
+	// Create pages to manage multiple screens
+	pages := tview.NewPages()
+
+	// Create menu
+	menu := tview.NewList().
+		AddItem("Commands", "Send commands to nodes", 'c', func() {
+			pages.SwitchToPage("commands")
+		}).ShowSecondaryText(false).
+		AddItem("Nodes", "View and manage nodes", 'n', func() {
+			pages.SwitchToPage("nodes")
+		}).ShowSecondaryText(false).
+		AddItem("Quit", "Press to exit", 'q', func() {
+			app.Stop()
+		})
+	menu.SetBorder(true).SetTitle("Menu")
+
+	// Create layout with menu on left and pages on right
+	flex := tview.NewFlex().
+		AddItem(menu, 20, 1, true).
+		AddItem(pages, 0, 1, false)
+
+	// Add pages
+	pages.AddPage("commands", createCommandPage(), true, true)
+	pages.AddPage("nodes", createNodesPage(), true, false)
+
+	// Global keyboard shortcuts
+	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+		switch event.Key() {
+		case tcell.KeyEsc:
+			// If on a page, go back to menu
+			if menu.HasFocus() {
+				app.Stop()
+			} else {
+				app.SetFocus(menu)
+			}
+			return nil
+		case tcell.KeyTab:
+			// Toggle between menu and current page
+			if menu.HasFocus() {
+				app.SetFocus(pages)
+			} else {
+				app.SetFocus(menu)
+			}
+			return nil
+		}
+		return event
+	})
+
+	// Update files list at startup
+	updateFilesList()
+
+	if err := app.SetRoot(flex, true).EnableMouse(true).Run(); err != nil {
+		panic(err)
+	}
+}
diff --git a/webui/index.html b/webui/index.html
new file mode 100644
index 0000000..dcbdb73
--- /dev/null
+++ b/webui/index.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Command Dashboard</title>
+    <link rel="stylesheet" href="styles.css" />
+    <style>
+      .popup {
+        display: none;
+        position: fixed;
+        z-index: 1000;
+        left: 0;
+        top: 0;
+        width: 100%;
+        height: 100%;
+        background-color: rgba(0, 0, 0, 0.5);
+      }
+
+      .popup-content {
+        background-color: #fefefe;
+        margin: 15% auto;
+        padding: 20px;
+        border: 1px solid #888;
+        width: 80%;
+        max-width: 600px;
+        position: relative;
+      }
+
+      .close-popup {
+        color: #aaa;
+        float: right;
+        font-size: 28px;
+        font-weight: bold;
+        cursor: pointer;
+      }
+
+      .close-popup:hover,
+      .close-popup:focus {
+        color: black;
+        text-decoration: none;
+        cursor: pointer;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="hamburger-menu">
+      <div class="hamburger-icon">
+        <span></span>
+        <span></span>
+        <span></span>
+      </div>
+    </div>
+
+    <nav class="side-menu">
+      <a href="index.html" class="active">Command</a>
+      <a href="#" id="fileTemplatesLink">File Templates</a>
+      <a href="settings.html">Settings</a>
+    </nav>
+
+    <div class="split-container">
+      <div class="left-panel">
+        <form id="commandForm">
+          <div class="form-group">
+            <label for="toNodes">To Nodes (comma-separated):</label>
+            <input
+              type="text"
+              id="toNodes"
+              placeholder="btdev1, btdev2..."
+              value="btdev1"
+            />
+          </div>
+
+          <div class="form-group">
+            <label for="jetstreamToNode">Jetstream To Node:</label>
+            <input type="text" id="jetstreamToNode" placeholder="btdev1" />
+          </div>
+
+          <div class="form-group">
+            <label for="useDetectedShell">Use Detected Shell:</label>
+            <select id="useDetectedShell">
+              <option value="false">false</option>
+              <option value="true">true</option>
+            </select>
+          </div>
+
+          <div class="form-group">
+            <label for="method">Method:</label>
+            <input type="text" id="method" value="cliCommand" />
+          </div>
+
+          <div class="form-group method-args">
+            <label>Method Arguments:</label>
+            <input
+              type="text"
+              class="method-arg"
+              placeholder="/bin/bash"
+              value="/bin/bash"
+            />
+            <input type="text" class="method-arg" placeholder="-c" value="-c" />
+            <textarea
+              class="method-arg"
+              placeholder="Enter command here..."
+              value="ls -l"
+            ></textarea>
+          </div>
+
+          <div class="form-group">
+            <label for="methodTimeout">Method Timeout (seconds):</label>
+            <input type="number" id="methodTimeout" value="3" />
+          </div>
+
+          <div class="form-group">
+            <label for="replyMethod">Reply Method:</label>
+            <input type="text" id="replyMethod" value="webUI" />
+          </div>
+
+          <div class="form-group">
+            <label for="ackTimeout">ACK Timeout:</label>
+            <input type="number" id="ackTimeout" value="0" />
+          </div>
+
+          <div class="button-group">
+            <button type="submit" id="sendBtn">Send</button>
+            <button type="submit" id="writeFileBtn">Write File</button>
+            <button type="button" id="generateBtn">Generate Command</button>
+          </div>
+        </form>
+      </div>
+      <div class="right-panel">
+        <textarea
+          id="outputArea"
+          readonly
+          placeholder="Command output will appear here..."
+        ></textarea>
+      </div>
+    </div>
+
+    <div id="fileTemplatesPopup" class="popup">
+      <div class="popup-content">
+        <span class="close-popup">&times;</span>
+        <!-- Content will go here later -->
+      </div>
+    </div>
+
+    <script
+      src="https://cdn.jsdelivr.net/npm/nats@2.29.1/index.min.js"
+      type="module"
+    ></script>
+    <script type="module" src="script.js"></script>
+    <script>
+      // Function to close menu
+      function closeMenu() {
+        document.body.classList.remove("menu-open");
+      }
+
+      // Toggle menu on hamburger click
+      document
+        .querySelector(".hamburger-menu")
+        .addEventListener("click", function (e) {
+          e.stopPropagation(); // Prevent click from immediately bubbling to document
+          document.body.classList.toggle("menu-open");
+        });
+
+      // Close menu when clicking outside
+      document.addEventListener("click", function (e) {
+        // If menu is open and click is outside the side-menu and hamburger-menu
+        if (
+          document.body.classList.contains("menu-open") &&
+          !e.target.closest(".side-menu") &&
+          !e.target.closest(".hamburger-menu")
+        ) {
+          closeMenu();
+        }
+      });
+
+      // Close menu on Escape key
+      document.addEventListener("keydown", function (e) {
+        if (
+          e.key === "Escape" &&
+          document.body.classList.contains("menu-open")
+        ) {
+          closeMenu();
+        }
+      });
+
+      // Prevent clicks on the menu itself from closing it
+      document
+        .querySelector(".side-menu")
+        .addEventListener("click", function (e) {
+          e.stopPropagation();
+        });
+
+      // File Templates popup functionality
+      const fileTemplatesLink = document.getElementById("fileTemplatesLink");
+      const fileTemplatesPopup = document.getElementById("fileTemplatesPopup");
+      const closePopup = document.querySelector(".close-popup");
+
+      fileTemplatesLink.addEventListener("click", function (e) {
+        e.preventDefault();
+        fileTemplatesPopup.style.display = "block";
+        closeMenu(); // Close the side menu when opening popup
+      });
+
+      closePopup.addEventListener("click", function () {
+        fileTemplatesPopup.style.display = "none";
+      });
+
+      // Close popup when clicking outside
+      window.addEventListener("click", function (e) {
+        if (e.target == fileTemplatesPopup) {
+          fileTemplatesPopup.style.display = "none";
+        }
+      });
+
+      // Add Escape key handler for popup
+      document.addEventListener("keydown", function (e) {
+        if (e.key === "Escape") {
+          if (fileTemplatesPopup.style.display === "block") {
+            fileTemplatesPopup.style.display = "none";
+          } else if (document.body.classList.contains("menu-open")) {
+            closeMenu();
+          }
+        }
+      });
+    </script>
+  </body>
+</html>
diff --git a/webui/main.go b/webui/main.go
new file mode 100644
index 0000000..a06ba65
--- /dev/null
+++ b/webui/main.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"net/http"
+)
+
+func main() {
+	// Define command line flag for port
+	port := flag.String("port", "localhost:8080", "Port to serve on (default 8080)")
+	flag.Parse()
+
+	// Create file server handler
+	fs := http.FileServer(http.Dir("."))
+
+	// Register handler for root path
+	http.Handle("/", fs)
+
+	// Start server
+	log.Printf("Starting server on %v", port)
+	if err := http.ListenAndServe(*port, nil); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/webui/script.js b/webui/script.js
new file mode 100644
index 0000000..957f33c
--- /dev/null
+++ b/webui/script.js
@@ -0,0 +1,471 @@
+/* jshint esversion: 9 */
+// import { connect } from 'https://cdn.jsdelivr.net/npm/nats@2.29.1/index.min.js';
+
+// import { wsconnect, nkeyAuthenticator } from "https://esm.run/@nats-io/nats-core";
+// import { createUser, fromPublic, fromSeed } from "https://esm.run/@nats-io/nkeys";
+
+import { wsconnect, nkeyAuthenticator } from "https://esm.run/@nats-io/nats-core";
+
+const commandForm = document.getElementById('commandForm');
+const generateBtn = document.getElementById('generateBtn');
+const sendBtn = document.getElementById('sendBtn');
+const writeFileBtn = document.getElementById('writeFileBtn');
+const outputArea = document.getElementById('outputArea');
+const fileTemplatesLink = document.getElementById('fileTemplatesLink');
+const fileTemplatesPopup = document.getElementById('fileTemplatesPopup');
+
+let nc;
+let filesList = new Map();
+let settings2;
+
+document.addEventListener('DOMContentLoaded', async function() {
+    const outputArea = document.getElementById('outputArea');
+    
+    // Check for, and load saved settings first
+    const savedSettings = localStorage.getItem("settings");
+
+    settings2 = JSON.parse(savedSettings);
+    // outputArea.value += "DEBUG: settings2: "+JSON.stringify(settings2)+"\n";
+    
+    // Initialize NATS client when the page loads.
+    try {
+        console.log("Connecting to NATS server: "+settings2.natsServer);
+        const connectOptions = {
+            servers: settings2.natsServer,
+            timeout: 3000
+        };
+
+        // Only add authenticator if we're using nkeys and have valid seed
+        if (settings2.useNkeys && settings2.nkeysSeedRAW) {
+            connectOptions.authenticator = nkeyAuthenticator(settings2.nkeysSeedRAW);
+            console.log("DEBUG: Using nkeys authentication");
+        }
+        console.log("******* settings.nkeysSeedRAW", settings2.nkeysSeedRAW);
+
+        nc = await wsconnect(connectOptions);
+        console.log("Connected to NATS server: "+settings2.natsServer);
+        
+        outputArea.value += "Connected to NATS server: "+settings2.natsServer+"\n";
+        
+        const sub = nc.subscribe(settings2.defaultNode+".webUI");
+        (async () => {
+            for await (const msg of sub) {
+                try {
+                    const msgJson = msg.json();
+                    if (!msgJson || !msgJson.data) {
+                        console.error("Invalid message format:", msg);
+                        continue;
+                    }
+
+                    // The data is a byte array for UTF8 characters that are base64 encoded.
+                    const ctrlMsgData = decodeBase64(msgJson.data);
+                    const ctrlMsgMethodInstructions = msgJson.methodInstructions || [];
+
+                    console.log("DEBUG: Received message:", {
+                        subject: msg.subject,
+                        data: ctrlMsgData,
+                        instructions: ctrlMsgMethodInstructions
+                    });
+
+                    if (ctrlMsgMethodInstructions[0] === "templateFilesList") {
+                        filesList = ctrlMsgData.split("\n");
+                        console.log("DEBUG: filesList:", filesList);
+
+                        // For each file in the list, we send a message to the central node
+                        for (const file of filesList) {
+                            if (!file.trim()) continue; // Skip empty lines
+
+                            console.log("DEBUG: Requesting content for file:", file);
+                            // Send a command to get the content of each file
+                            const js2 = {
+                                "toNodes": ["central"],
+                                "useDetectedShell": false,
+                                "method": "cliCommand",
+                                "methodArgs": ["/bin/bash","-c","cat files/"+file],
+                                "methodTimeout": 3,
+                                "fromNode": settings2.defaultNode,
+                                "replyMethod": "webUI",
+                                "replyMethodInstructions": ["templateFileContent",file],
+                                "ACKTimeout": 0
+                            };
+                            nc.publish("central.cliCommand", JSON.stringify(js2));
+                        }
+                    }
+
+                    // Handle file content responses
+                    if (ctrlMsgMethodInstructions[0] === "templateFileContent" && ctrlMsgMethodInstructions[1]) {
+                        const file = ctrlMsgMethodInstructions[1];
+                        const fileContent = ctrlMsgData;
+                        console.log("DEBUG: Storing file content in localStorage:", {
+                            file: file,
+                            contentLength: fileContent.length
+                        });
+
+                        // Get existing filelist or create new one
+                        let filelist = {};
+                        const existingFilelist = localStorage.getItem('filelist');
+                        if (existingFilelist) {
+                            filelist = JSON.parse(existingFilelist);
+                        }
+                        
+                        // Add or update file in filelist
+                        filelist[file] = {
+                            content: fileContent,
+                            lastModified: new Date().toISOString()
+                        };
+                        
+                        // Store updated filelist
+                        localStorage.setItem('filelist', JSON.stringify(filelist));
+                    }
+
+                    outputArea.value += `Received message on ${msg.subject}:\n${ctrlMsgData}\n`;
+                    
+                    // Scroll to bottom
+                    outputArea.scrollTop = outputArea.scrollHeight;
+                } catch (error) {
+                    console.error("Error processing message:", error);
+                    outputArea.value += `\nError processing message: ${error.message}\n`;
+                }
+            }
+        })().catch((err) => {
+            console.error("Subscription error:", err);
+            outputArea.value += `\nSubscription error: ${err.message}\n`;
+        });
+
+        // Handle connection close
+        nc.closed().then((err) => {
+            if (err) {
+                outputArea.value += `\nNATS connection closed with error: ${err.message}\n`;
+            } else {
+                outputArea.value += "\nNATS connection closed\n";
+            }
+        });
+
+    } catch (err) {
+        outputArea.value += `Failed to connect to NATS server: ${err.message}\n`;
+    }
+
+    // Send a command to list the files in the files folder.
+    // We will later use the list we get back to ask for the content of each file.
+    const js2 = {"toNodes": ["central"],"useDetectedShell": false,"method": "cliCommand","methodArgs": ["/bin/bash","-c","ls -1 files/"],
+        "methodTimeout": 3,
+        "fromNode": settings2.defaultNode,
+        "replyMethod": "webUI",
+        "replyMethodInstructions": ["templateFilesList"],
+        "ACKTimeout": 0
+    };
+    nc.publish("central.cliCommand", JSON.stringify(js2));
+
+    // localStorage.clear();
+
+    // Disable default form submission
+    commandForm.onsubmit = function(e) {
+        e.preventDefault();
+        return false;
+    };
+
+    // Disable Enter key submission for all inputs except textarea
+    commandForm.addEventListener('keypress', function(e) {
+        if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
+            e.preventDefault();
+            return false;
+        }
+    }, true);
+
+    initSplitter();
+}); 
+
+function generateCommandData() {
+    const methodArgs = Array.from(document.querySelectorAll('.method-arg'))
+        .map(arg => arg.value)
+        .filter(value => value !== ''); // Remove empty values
+
+    return {
+        toNodes: document.getElementById('toNodes').value.split(',').map(node => node.trim()).filter(node => node !== ''),
+        jetstreamToNode: document.getElementById('jetstreamToNode').value || undefined,
+        useDetectedShell: document.getElementById('useDetectedShell').value === 'true',
+        method: document.getElementById('method').value,
+        methodArgs: methodArgs,
+        methodTimeout: parseInt(document.getElementById('methodTimeout').value),
+        fromNode: settings2.defaultNode,
+        replyMethod: document.getElementById('replyMethod').value,
+        ACKTimeout: parseInt(document.getElementById('ackTimeout').value)
+    };
+}
+
+// Handle Send button click
+sendBtn.addEventListener('click', function(e) {
+    e.preventDefault();
+
+    if (!nc) {
+        outputArea.value += "Error: Not connected to NATS server\n";
+        return false;
+    }
+
+    const formData = generateCommandData();
+    
+    // Check if Method Arg 3 contains a CTRL_FILE template
+    if (formData.methodArgs[2] && formData.methodArgs[2].includes('{{CTRL_FILE:')) {
+        // Extract the file path from the template
+        const match = formData.methodArgs[2].match(/{{CTRL_FILE:([^}]+)}}/);
+        if (match) {
+            const filePath = match[1];
+            const fileName = filePath.split('/').pop(); // Get just the filename
+            const filelist = JSON.parse(localStorage.getItem('filelist') || '{}');
+            const fileContent = filelist[fileName] && filelist[fileName].content;
+            
+            if (fileContent) {
+                // Replace the entire CTRL_FILE clause with the file content
+                formData.methodArgs[2] = formData.methodArgs[2].replace(/{{CTRL_FILE:[^}]+}}/, fileContent);
+            } else {
+                outputArea.value += `\nError: File content not found for ${fileName}\n`;
+                return false;
+            }
+        }
+    }
+
+    // Send the command to the NATS server
+    for (const toNode of formData.toNodes) {
+        console.log("Sending command to "+toNode);
+        nc.publish(toNode+"."+formData.method, JSON.stringify(formData));
+    }
+
+    return false;
+});
+
+// Handle Generate Command button click
+generateBtn.addEventListener('click', function(e) {
+    e.preventDefault();
+
+    const formData = generateCommandData();
+    const output = formatCommandYaml(formData);
+
+    outputArea.value += output;
+    return false;
+});
+
+// Handle Write File button click
+writeFileBtn.addEventListener('click', function(e) {
+    e.preventDefault();
+    const formData = generateCommandData();
+    const output = formatCommandYaml(formData);
+    outputArea.value += output;
+
+    // Use hardcoded path instead of reading from input
+    writeToFile(output, '/Users/bt/ctrl/tmp/bt1/readfolder/command.yaml');
+    
+    return false;
+});
+
+// Add this after the other event listeners in the DOMContentLoaded function
+
+fileTemplatesLink.addEventListener("click", function (e) {
+    e.preventDefault();
+    
+    // Get the popup content div and clear it
+    const popupContent = document.querySelector('.popup-content');
+    
+    // Get filelist from localStorage
+    const filelist = JSON.parse(localStorage.getItem('filelist') || '{}');
+    const fileListHTML = Object.keys(filelist)
+        .map(fileName => 
+            fileName.trim() ? `<div class="file-item">${fileName}</div>` : ''
+        ).join('');
+    
+    // Add title and container for files
+    popupContent.innerHTML = `
+        <span class="close-popup">&times;</span>
+        <h2>File Templates</h2>
+        <div class="files-list">
+            ${fileListHTML}
+        </div>
+    `;
+
+    // Re-attach close button handler since we replaced the content
+    document.querySelector('.close-popup').addEventListener('click', function() {
+        fileTemplatesPopup.style.display = "none";
+    });
+
+    // Add click handlers for file items
+    document.querySelectorAll('.file-item').forEach(item => {
+        item.addEventListener('click', () => {
+            const methodArg3 = document.querySelector('.method-arg:last-child');
+            methodArg3.value = `{{CTRL_FILE:files/${item.textContent}}}`;
+            fileTemplatesPopup.style.display = "none";
+        });
+    });
+
+    fileTemplatesPopup.style.display = "block";
+    closeMenu();
+});
+
+// Add keyboard shortcut for template popup
+document.addEventListener('keydown', function(e) {
+    // Check if Ctrl+T was pressed (and not Cmd+T on Mac which is for new tab)
+    if (e.ctrlKey && e.key === 't') {
+        e.preventDefault(); // Prevent default browser behavior
+        
+        // Show the template popup
+        fileTemplatesPopup.style.display = "block";
+        
+        // Get the popup content div and clear it
+        const popupContent = document.querySelector('.popup-content');
+        
+        // Get filelist from localStorage
+        const filelist = JSON.parse(localStorage.getItem('filelist') || '{}');
+        const fileListHTML = Object.keys(filelist)
+            .map(fileName => 
+                fileName.trim() ? `<div class="file-item">${fileName}</div>` : ''
+            ).join('');
+        
+        // Add title and container for files
+        popupContent.innerHTML = `
+            <span class="close-popup">&times;</span>
+            <h2>File Templates</h2>
+            <div class="files-list">
+                ${fileListHTML}
+            </div>
+        `;
+
+        // Re-attach close button handler
+        document.querySelector('.close-popup').addEventListener('click', function() {
+            fileTemplatesPopup.style.display = "none";
+        });
+
+        // Add click handlers for file items
+        document.querySelectorAll('.file-item').forEach(item => {
+            item.addEventListener('click', () => {
+                const methodArg3 = document.querySelector('.method-arg:last-child');
+                methodArg3.value = `{{CTRL_FILE:files/${item.textContent}}}`;
+                fileTemplatesPopup.style.display = "none";
+            });
+        });
+    }
+});
+
+function writeToFile(content, filename) {
+    // Create a Blob containing the text content
+    const blob = new Blob([content], { type: 'text/plain' });
+    
+    // Create a URL for the Blob
+    const url = globalThis.URL.createObjectURL(blob);
+    
+    // Create a temporary anchor element
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = filename;  // Use the provided filename
+    
+    // Append link to body, click it, and remove it
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    
+    // Clean up by revoking the Blob URL
+    globalThis.URL.revokeObjectURL(url);
+}
+
+
+function decodeBase64(base64) {
+    const text = atob(base64);
+    const length = text.length;
+    const bytes = new Uint8Array(length);
+    for (let i = 0; i < length; i++) {
+        bytes[i] = text.charCodeAt(i);
+    }
+    const decoder = new TextDecoder(); // default is utf-8
+    return decoder.decode(bytes);
+}
+
+function formatCommandYaml(commandData) {
+    const output = `---
+- toNodes:
+    - ${commandData.toNodes[0]}
+  ${commandData.jetstreamToNode ? `jetstreamToNode: ${commandData.jetstreamToNode}` : '#jetstreamToNode: btdev1'}
+  useDetectedShell: ${commandData.useDetectedShell}
+  method: ${commandData.method}
+  methodArgs:
+    - ${commandData.methodArgs.join('\n    - ')}
+  methodTimeout: ${commandData.methodTimeout}
+  replyMethod: ${commandData.replyMethod}
+  ACKTimeout: ${commandData.ACKTimeout}`;
+  
+  return output;
+}
+
+// Add splitter functionality
+function initSplitter() {
+    const splitter = document.createElement('div');
+    splitter.className = 'splitter';
+    
+    const leftPanel = document.querySelector('.left-panel');
+    const container = document.querySelector('.split-container');
+    
+    let isDragging = false;
+
+    splitter.addEventListener('mousedown', function(e) {
+        isDragging = true;
+        document.body.style.cursor = 'col-resize';
+        
+        document.addEventListener('mousemove', handleMouseMove);
+        document.addEventListener('mouseup', handleMouseUp);
+        
+        // Prevent text selection while dragging
+        e.preventDefault();
+    });
+
+    function handleMouseMove(e) {
+        if (!isDragging) return;
+
+        const containerRect = container.getBoundingClientRect();
+        let newWidth = e.clientX - containerRect.left;
+        
+        // Calculate percentage width
+        const containerWidth = containerRect.width;
+        const minWidth = 200;
+        const maxWidth = containerWidth - 200;
+
+        // Constrain the width
+        newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth));
+        
+        leftPanel.style.width = `${newWidth}px`;
+    }
+
+    function handleMouseUp() {
+        isDragging = false;
+        document.body.style.cursor = '';
+        document.removeEventListener('mousemove', handleMouseMove);
+        document.removeEventListener('mouseup', handleMouseUp);
+    }
+
+    // Insert splitter between panels
+    leftPanel.after(splitter);
+}
+
+// Add these styles to the existing style section in the head
+const style = document.createElement('style');
+style.textContent = `
+    .files-list {
+        margin-top: 20px;
+        max-height: 400px;
+        overflow-y: auto;
+    }
+
+    .file-item {
+        padding: 10px;
+        border: 1px solid #ddd;
+        margin-bottom: 5px;
+        border-radius: 4px;
+        cursor: pointer;
+        transition: background-color 0.2s;
+    }
+
+    .file-item:hover {
+        background-color: #f0f0f0;
+    }
+
+    .popup-content h2 {
+        margin-top: 0;
+        margin-bottom: 20px;
+    }
+`;
+document.head.appendChild(style);
diff --git a/webui/settings.html b/webui/settings.html
new file mode 100644
index 0000000..23afc8a
--- /dev/null
+++ b/webui/settings.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Settings - Command Dashboard</title>
+    <link rel="stylesheet" href="styles.css" />
+  </head>
+  <body>
+    <div class="hamburger-menu">
+      <div class="hamburger-icon">
+        <span></span>
+        <span></span>
+        <span></span>
+      </div>
+    </div>
+
+    <nav class="side-menu">
+      <a href="index.html">Command</a>
+      <a href="#" id="fileTemplatesLink">File Templates</a>
+      <a href="settings.html" class="active">Settings</a>
+    </nav>
+
+    <div class="settings-container">
+      <h1>Settings</h1>
+      <button id="generateNkeysBtn" class="settings-button">
+        Generate Nkeys
+      </button>
+      <div class="form-group nkeys-group">
+        <div class="form-group">
+          <label for="nkeysSeedRaw">Nkeys SeedRaw:</label>
+          <input
+            type="text"
+            id="nkeysSeedRaw"
+            placeholder="Enter seed raw value..."
+          />
+          <button id="updateKeysBtn" class="settings-button">
+            Update Keys
+          </button>
+        </div>
+        <div class="form-group">
+          <label for="nkeysSeed">Nkeys Seed:</label>
+          <input type="text" id="nkeysSeed" readonly />
+        </div>
+        <div class="form-group">
+          <label for="nkeysPublic">Nkeys Public:</label>
+          <input type="text" id="nkeysPublic" readonly />
+        </div>
+        <div class="form-group">
+          <label for="useNkeys">
+            <input type="checkbox" id="useNkeys" />
+            Use Nkeys
+          </label>
+        </div>
+      </div>
+      <form id="settingsForm">
+        <div class="form-group">
+          <label for="natsServer">NATS Server URL:</label>
+          <input type="text" id="natsServer" value="ws://localhost:4223" />
+        </div>
+        <div class="form-group">
+          <label for="defaultNode">Default Node:</label>
+          <input type="text" id="defaultNode" value="btdev1" />
+        </div>
+      </form>
+      <button id="saveSettingsBtn" class="settings-button">
+        Save Settings
+      </button>
+      <button id="clearAllBtn" class="settings-button clear-button">
+        Clear All Information
+      </button>
+    </div>
+
+    <script type="module" src="settings.js"></script>
+  </body>
+</html>
diff --git a/webui/settings.js b/webui/settings.js
new file mode 100644
index 0000000..5f21d59
--- /dev/null
+++ b/webui/settings.js
@@ -0,0 +1,150 @@
+/* jshint esversion: 9 */
+import { createUser, fromPublic, fromSeed } from "https://esm.run/@nats-io/nkeys";
+// import { createUser, fromSeed } from "https://cdn.jsdelivr.net/npm/@nats.io/nkeys@3.1.1/lib/nkeys.js";
+
+const hamburgerMenu = document.querySelector(".hamburger-menu");
+const generateNkeysBtn = document.getElementById("generateNkeysBtn");
+const saveSettingsBtn = document.getElementById("saveSettingsBtn");
+const natsServerInput = document.getElementById("natsServer");
+const defaultNodeInput = document.getElementById("defaultNode");
+const nkeysSeedInput = document.getElementById("nkeysSeed");
+const nkeysPublicInput = document.getElementById("nkeysPublic");
+const useNkeysCheckbox = document.getElementById("useNkeys");
+const clearAllBtn = document.getElementById("clearAllBtn");
+const nkeysSeedRawInput = document.getElementById("nkeysSeedRaw");
+const updateKeysBtn = document.getElementById("updateKeysBtn");
+
+// Global settings object
+const settings = {
+    natsServer: "ws://localhost:4223",
+    defaultNode: "btdev1",
+    nkeysSeed: "",
+    nkeysPublic: "",
+    useNkeys: false,
+    nkeysSeedRAW: null
+};
+
+// Add input event listeners to update settings object
+natsServerInput.addEventListener('input', () => {
+    settings.natsServer = natsServerInput.value;
+});
+
+defaultNodeInput.addEventListener('input', () => {
+    settings.defaultNode = defaultNodeInput.value;
+});
+
+useNkeysCheckbox.addEventListener('change', () => {
+    settings.useNkeys = useNkeysCheckbox.checked;
+});
+
+hamburgerMenu.addEventListener("click", function () {
+    document.body.classList.toggle("menu-open");
+});
+
+// Add click handler for the generate nkeys button
+generateNkeysBtn.addEventListener("click", function () {
+    // create an user nkey KeyPair
+    const user = createUser();
+    // A seed is the public and private keys together.
+    const seed = user.getSeed();
+
+    // console.log("DEBUG: seed array:", Array.from(seed));
+    const publicKey = user.getPublicKey();
+
+    // Update settings object
+    settings.nkeysSeed = new TextDecoder().decode(seed);
+    settings.nkeysPublic = publicKey;
+    settings.nkeysSeedRAW = Array.from(seed); // Store as regular array for JSON serialization
+    
+    // console.log("DEBUG: settings.nkeysSeedRAW:", settings.nkeysSeedRAW);
+    
+    // Update the input fields
+    nkeysSeedInput.value = settings.nkeysSeed;
+    nkeysPublicInput.value = settings.nkeysPublic;
+    nkeysSeedRawInput.value = settings.nkeysSeedRAW.join(',');
+});
+
+// Load existing settings when page loads
+document.addEventListener("DOMContentLoaded", function () {
+    const savedSettings = localStorage.getItem("settings");
+    
+    if (savedSettings) {
+        // Update our settings object with saved values
+        const loadedSettings = JSON.parse(savedSettings);
+        Object.assign(settings, loadedSettings);
+
+        // Update UI with values from settings object
+        natsServerInput.value = settings.natsServer;
+        defaultNodeInput.value = settings.defaultNode;
+        nkeysSeedInput.value = settings.nkeysSeed || "";
+        nkeysPublicInput.value = settings.nkeysPublic || "";
+        nkeysSeedRawInput.value = settings.nkeysSeedRAW ? settings.nkeysSeedRAW.join(',') : "";
+        useNkeysCheckbox.checked = settings.useNkeys;
+        
+        if (settings.nkeysSeedRAW) {
+            // Convert the stored array back to Uint8Array
+            settings.nkeysSeedRAW = Uint8Array.from(settings.nkeysSeedRAW);
+        }
+    }
+
+    // console.log("DEBUG: Loaded settings:", settings);
+    // console.log("DEBUG: Loaded nkeysSeedRAW:", settings.nkeysSeedRAW ? Array.from(settings.nkeysSeedRAW) : "not set");
+});
+
+// Handle save settings button click
+saveSettingsBtn.addEventListener("click", function () {
+    // Save settings object directly to localStorage
+    localStorage.setItem("settings", JSON.stringify(settings));
+
+    // Show save confirmation
+    alert("Settings saved successfully!");
+});
+
+// Handle clear all button click
+clearAllBtn.addEventListener("click", function () {
+    // Show confirmation dialog
+    if (confirm("Are you sure you want to clear all information? This will delete all settings and file templates.")) {
+        // Clear localStorage
+        localStorage.clear();
+        
+        // Reset settings object to defaults
+        settings.natsServer = "ws://localhost:4223";
+        settings.defaultNode = "btdev1";
+        settings.nkeysSeed = "";
+        settings.nkeysPublic = "";
+        settings.useNkeys = false;
+        settings.nkeysSeedRAW = null;
+        
+        // Reset form fields
+        natsServerInput.value = settings.natsServer;
+        defaultNodeInput.value = settings.defaultNode;
+        nkeysSeedInput.value = "";
+        nkeysPublicInput.value = "";
+        useNkeysCheckbox.checked = false;
+        
+        alert("All information has been cleared.");
+    }
+});
+
+// Add click handler for update keys button
+updateKeysBtn.addEventListener('click', function() {
+    try {
+        // Convert input string to array of numbers
+        const rawArray = nkeysSeedRawInput.value.split(',').map(num => parseInt(num.trim()));
+        const seedRaw = new Uint8Array(rawArray);
+        
+        // Create key pair from seed
+        const keyPair = fromSeed(seedRaw);
+        
+        // Update settings and fields
+        settings.nkeysSeedRAW = Array.from(seedRaw);
+        settings.nkeysSeed = new TextDecoder().decode(seedRaw);
+        settings.nkeysPublic = keyPair.getPublicKey();
+        
+        nkeysSeedInput.value = settings.nkeysSeed;
+        nkeysPublicInput.value = settings.nkeysPublic;
+    } catch (error) {
+        console.error("Invalid seed raw value:", error);
+        alert("Invalid seed raw value. Please check the format.");
+    }
+}); 
\ No newline at end of file
diff --git a/webui/styles.css b/webui/styles.css
new file mode 100644
index 0000000..61b6c5d
--- /dev/null
+++ b/webui/styles.css
@@ -0,0 +1,319 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+  background-color: #f5f6fa;
+  height: 100vh;
+  overflow: hidden;
+}
+
+.split-container {
+  display: flex;
+  height: 100vh;
+  position: relative;
+  padding-left: 60px;
+}
+
+.left-panel {
+  min-width: 200px;
+  padding: 20px;
+  overflow-y: auto;
+  font-family: "Courier New", monospace;
+  font-size: 14px;
+  width: 50%; /* Set initial width */
+}
+
+.right-panel {
+  flex: 1; /* Take remaining space */
+  padding: 20px;
+  background-color: #2c3e50;
+  min-width: 200px;
+}
+
+#outputArea {
+  width: 100%;
+  height: 100%;
+  background-color: #34495e;
+  color: #ecf0f1;
+  font-family: "Courier New", monospace;
+  padding: 15px;
+  border: none;
+  border-radius: 4px;
+  resize: none;
+  white-space: pre-wrap;
+  font-size: 14px;
+  line-height: 1.4;
+}
+
+form {
+  background-color: white;
+  padding: 30px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.form-group {
+  margin-bottom: 20px;
+}
+
+label {
+  display: block;
+  margin-bottom: 8px;
+  font-weight: 600;
+  color: #2c3e50;
+  font-family: "Courier New", monospace;
+  font-size: 14px;
+}
+
+input,
+select,
+textarea {
+  width: 100%;
+  padding: 10px;
+  border: 1px solid #dcdde1;
+  border-radius: 4px;
+  font-family: "Courier New", monospace;
+  font-size: 14px;
+}
+
+textarea {
+  min-height: 100px;
+  resize: vertical;
+}
+
+.method-args input,
+.method-args textarea {
+  margin-bottom: 10px;
+}
+
+button {
+  background-color: #3498db;
+  color: white;
+  padding: 12px 24px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-family: "Courier New", monospace;
+  font-size: 14px;
+  transition: background-color 0.3s;
+}
+
+button:hover {
+  background-color: #2980b9;
+}
+
+.active-connections {
+  background-color: white;
+  padding: 30px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+#connectionsList {
+  display: grid;
+  gap: 20px;
+}
+
+.connection-item {
+  border: 1px solid var(--border-color);
+  padding: 15px;
+  border-radius: 4px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.connection-info {
+  flex-grow: 1;
+}
+
+.connection-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.delete-btn {
+  background-color: #e74c3c;
+}
+
+.delete-btn:hover {
+  background-color: #c0392b;
+}
+
+.button-group {
+  display: flex;
+  gap: 10px;
+}
+
+.button-group button {
+  flex: 1;
+}
+
+.splitter {
+  width: 8px;
+  background-color: #cbd5e0;
+  cursor: col-resize;
+  transition: background-color 0.2s;
+  user-select: none; /* Prevent text selection while dragging */
+}
+
+.splitter:hover,
+.splitter.dragging {
+  background-color: #3498db;
+}
+
+/* Hamburger Menu Styles */
+.hamburger-menu {
+  position: fixed;
+  top: 20px;
+  left: 20px;
+  z-index: 100;
+  cursor: pointer;
+}
+
+.hamburger-icon {
+  width: 30px;
+  height: 20px;
+  position: relative;
+  cursor: pointer;
+}
+
+.hamburger-icon span {
+  display: block;
+  position: absolute;
+  height: 3px;
+  width: 100%;
+  background: #2c3e50;
+  border-radius: 3px;
+  transition: all 0.3s ease;
+}
+
+.hamburger-icon span:nth-child(1) {
+  top: 0;
+}
+.hamburger-icon span:nth-child(2) {
+  top: 8px;
+}
+.hamburger-icon span:nth-child(3) {
+  top: 16px;
+}
+
+.menu-open .hamburger-icon span:nth-child(1) {
+  transform: rotate(45deg);
+  top: 8px;
+}
+
+.menu-open .hamburger-icon span:nth-child(2) {
+  opacity: 0;
+}
+
+.menu-open .hamburger-icon span:nth-child(3) {
+  transform: rotate(-45deg);
+  top: 8px;
+}
+
+.side-menu {
+  position: fixed;
+  top: 0;
+  left: -250px;
+  width: 250px;
+  height: 100vh;
+  background: #2c3e50;
+  padding-top: 60px;
+  transition: left 0.3s ease;
+  z-index: 99;
+}
+
+.menu-open .side-menu {
+  left: 0;
+}
+
+.side-menu a {
+  display: block;
+  padding: 15px 25px;
+  color: white;
+  text-decoration: none;
+  font-family: "Courier New", monospace;
+  font-size: 14px;
+  transition: background-color 0.3s;
+}
+
+.side-menu a:hover {
+  background: #34495e;
+}
+
+.side-menu a.active {
+  background: #3498db;
+}
+
+.settings-container {
+  padding: 20px;
+  margin-left: 60px; /* Space for the hamburger menu */
+}
+
+.settings-container h1 {
+  margin-bottom: 30px;
+  color: #2c3e50;
+  font-family: "Courier New", monospace;
+  font-size: 24px;
+}
+
+#settingsForm {
+  background: white;
+  padding: 30px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.settings-button {
+  padding: 10px 20px;
+  background-color: #4caf50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 16px;
+  margin: 10px 0;
+}
+
+.settings-button:hover {
+  background-color: #45a049;
+}
+
+.clear-button {
+  background-color: #e74c3c;
+  margin-left: 10px;
+}
+
+.clear-button:hover {
+  background-color: #c0392b;
+}
+
+.nkeys-group {
+  background: white;
+  padding: 20px;
+  border-radius: 8px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  margin: 20px 0;
+}
+
+.nkeys-group input[readonly] {
+  background-color: #f5f5f5;
+  cursor: text;
+}
+
+/* Checkbox styling */
+.nkeys-group .form-group label[for="useNkeys"] {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.nkeys-group .form-group input[type="checkbox"] {
+  width: auto;
+  margin: 0;
+}