diff --git a/app/backend/internal/handler/dashboard.go b/app/backend/internal/handler/dashboard.go index ee10bf6..40b8bd1 100644 --- a/app/backend/internal/handler/dashboard.go +++ b/app/backend/internal/handler/dashboard.go @@ -1,6 +1,8 @@ package handler import ( + "strconv" + "github.com/gofiber/fiber/v2" "git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model" "git.tukangketik.id/swanadiva/hostkeeper/v2/internal/store" @@ -20,12 +22,12 @@ var navItems = []fiber.Map{ func Dashboard(c *fiber.Ctx) error { hosts := hostStore.All() return c.Render("index", fiber.Map{ - "View": "hosts", - "Title": "Hosts", - "Hosts": hosts, - "Active": countByStatus(hosts, "active"), - "Offline": countByStatus(hosts, "offline"), - "NavItems": navItems, + "View": "hosts", + "Title": "Hosts", + "Hosts": hosts, + "Active": countByStatus(hosts, "active"), + "Offline": countByStatus(hosts, "offline"), + "NavItems": navItems, }) } @@ -34,28 +36,103 @@ func DashboardList(c *fiber.Ctx) error { filter := c.Query("filter", "all") hosts := hostStore.Search(q, filter) return c.Render("host_list", fiber.Map{ - "Hosts": hosts, - "Active": countByStatus(hosts, "active"), + "Hosts": hosts, + "Active": countByStatus(hosts, "active"), "Offline": countByStatus(hosts, "offline"), }) } +func parsePort(s string, def int) int { + i, err := strconv.Atoi(s) + if err != nil || i < 1 || i > 65535 { + return def + } + return i +} + func CreateHost(c *fiber.Ctx) error { name := c.FormValue("name") ip := c.FormValue("ip") - os := c.FormValue("os") - provider := c.FormValue("provider") - hostType := c.FormValue("type") - if name == "" || ip == "" { return c.Status(400).SendString("name and ip required") } - hostStore.Add(name, ip, os, provider, hostType) + h := model.Host{ + Name: name, + IP: ip, + OS: c.FormValue("os"), + Provider: c.FormValue("provider"), + Type: c.FormValue("type"), + Port: parsePort(c.FormValue("port"), 22), + Username: c.FormValue("username"), + AuthType: c.FormValue("authType"), + Password: c.FormValue("password"), + Hostname: c.FormValue("hostname"), + Notes: c.FormValue("notes"), + } + if h.Username == "" { + h.Username = "root" + } + if h.AuthType == "" { + h.AuthType = "key" + } + + created := hostStore.AddFull(h) + hosts := hostStore.All() + _ = created + return c.Render("host_list", fiber.Map{ + "Hosts": hosts, + "Active": countByStatus(hosts, "active"), + "Offline": countByStatus(hosts, "offline"), + }) +} + +func UpdateHost(c *fiber.Ctx) error { + id := c.Params("id") + existing, found := hostStore.Get(id) + if !found { + return c.Status(404).SendString("host not found") + } + + if v := c.FormValue("name"); v != "" { + existing.Name = v + } + if v := c.FormValue("ip"); v != "" { + existing.IP = v + } + if v := c.FormValue("os"); v != "" { + existing.OS = v + } + if v := c.FormValue("provider"); v != "" { + existing.Provider = v + } + if v := c.FormValue("type"); v != "" { + existing.Type = v + } + if v := c.FormValue("port"); v != "" { + existing.Port = parsePort(v, 22) + } + if v := c.FormValue("username"); v != "" { + existing.Username = v + } + if v := c.FormValue("authType"); v != "" { + existing.AuthType = v + } + if v := c.FormValue("password"); v != "" { + existing.Password = v + } + if v := c.FormValue("hostname"); v != "" { + existing.Hostname = v + } + if v := c.FormValue("notes"); v != "" { + existing.Notes = v + } + + hostStore.Update(existing) hosts := hostStore.All() return c.Render("host_list", fiber.Map{ - "Hosts": hosts, - "Active": countByStatus(hosts, "active"), + "Hosts": hosts, + "Active": countByStatus(hosts, "active"), "Offline": countByStatus(hosts, "offline"), }) } diff --git a/app/backend/internal/handler/terminal.go b/app/backend/internal/handler/terminal.go index 09e349e..2742faa 100644 --- a/app/backend/internal/handler/terminal.go +++ b/app/backend/internal/handler/terminal.go @@ -28,12 +28,14 @@ func Terminal(c *fiber.Ctx) error { } } hostsJSON, _ := json.Marshal(available) + selectedHost := c.Query("host", "") data := fiber.Map{ - "View": "terminal", - "Title": "Terminal", - "NavItems": navItems, - "Hosts": available, - "HostsJSON": string(hostsJSON), + "View": "terminal", + "Title": "Terminal", + "NavItems": navItems, + "Hosts": available, + "HostsJSON": string(hostsJSON), + "SelectedHost": selectedHost, } if c.Get("HX-Request") != "" { return c.Render("terminal_content", data) diff --git a/app/backend/main.go b/app/backend/main.go index 3b27ef5..d90d6d8 100644 --- a/app/backend/main.go +++ b/app/backend/main.go @@ -44,6 +44,12 @@ func main() { } return m }) + engine.AddFunc("default", func(def, val interface{}) interface{} { + if val == nil || val == "" { + return def + } + return val + }) app := fiber.New(fiber.Config{ Views: engine, @@ -57,6 +63,7 @@ func main() { app.Get("/", handler.Dashboard) app.Get("/hosts", handler.DashboardList) app.Post("/hosts", handler.CreateHost) + app.Put("/hosts/:id", handler.UpdateHost) app.Delete("/hosts/:id", handler.DeleteHost) app.Get("/snippets", handler.Snippets) app.Get("/snippets/grid", handler.SnippetGrid) diff --git a/app/backend/static/js/terminal.js b/app/backend/static/js/terminal.js index 44d5440..9e2b5f2 100644 --- a/app/backend/static/js/terminal.js +++ b/app/backend/static/js/terminal.js @@ -1,6 +1,8 @@ document.addEventListener('alpine:init', () => { const el = document.getElementById('hosts-data'); const hostOptions = el ? JSON.parse(el.textContent) : []; + const selEl = document.getElementById('selected-host'); + const selectedHostId = selEl ? JSON.parse(selEl.textContent) : ''; function processCommand(cmd, tabName) { const lines = []; @@ -86,9 +88,15 @@ document.addEventListener('alpine:init', () => { }, addTab() { - const host = hostOptions.length > 0 - ? hostOptions[Math.floor(Math.random() * hostOptions.length)] - : { id: 'h1', name: 'localhost', ip: '127.0.0.1' }; + let host; + if (selectedHostId) { + host = hostOptions.find(h => h.id === selectedHostId) || hostOptions[0]; + } + if (!host) { + host = hostOptions.length > 0 + ? hostOptions[Math.floor(Math.random() * hostOptions.length)] + : { id: 'h1', name: 'localhost', ip: '127.0.0.1' }; + } const id = 'tab-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6); this.tabs.push({ id, name: host.name, host: host.ip, connected: true }); this.activeTab = this.tabs.length - 1; diff --git a/app/backend/views/host_list.html b/app/backend/views/host_list.html index b0cd3fa..6f1f44a 100644 --- a/app/backend/views/host_list.html +++ b/app/backend/views/host_list.html @@ -3,7 +3,7 @@
{{range .Hosts}} -
+
@@ -15,19 +15,29 @@

{{.Name}}

-

{{.IP}}

+

{{.IP}}

+

+ {{.Username | default "root"}}@{{if .Hostname}}{{.Hostname}}{{else}}{{.IP}}{{end}}{{if ne .Port 22}}:{{.Port}}{{end}} +

{{.OS}} {{.Provider}} + {{if .Type}}{{.Type}}{{end}}
Last seen {{.LastSeen}}
- Connect + Connect +
@@ -46,7 +56,7 @@
{{range .Hosts}} -
+
@@ -65,14 +75,20 @@
-
+
{{.Status}} - Connect + Connect +
@@ -88,8 +104,9 @@ {{if eq (len .Hosts) 0}}
+

No hosts found

-

Try a different search or add a new host

+

Add your first server to get started

{{end}}
diff --git a/app/backend/views/host_modal.html b/app/backend/views/host_modal.html index d7436d1..415823d 100644 --- a/app/backend/views/host_modal.html +++ b/app/backend/views/host_modal.html @@ -1,53 +1,85 @@ {{define "host_modal"}} -
+
-
-
-

Add Host Credentials

+
+ +
+

-
-
- - + + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
- - + +
-
-
@@ -55,27 +87,100 @@
- - -
- + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ +
+ class="bg-primary text-white py-2 px-5 rounded-lg font-bold text-xs shadow-md hover:bg-primary-container hover:text-on-primary-container transition-all" + x-text="editing ? 'Save Changes' : 'Add Host'">
-{{end}} + + +{{end}} \ No newline at end of file diff --git a/app/backend/views/index.html b/app/backend/views/index.html index 84506fb..f87385c 100644 --- a/app/backend/views/index.html +++ b/app/backend/views/index.html @@ -109,5 +109,15 @@
+ + +
+ + +
+ diff --git a/app/backend/views/terminal.html b/app/backend/views/terminal.html index 931deec..6c28af5 100644 --- a/app/backend/views/terminal.html +++ b/app/backend/views/terminal.html @@ -31,6 +31,7 @@
+