feat: Host CRUD complete — edit, full form fields, connect, toast

- Add UpdateHost handler + PUT /hosts/:id route
- CreateHost now accepts all fields (port, username, auth, notes)
- host_modal.html: full form with port, username, auth type toggle, edit mode
- host_list.html: edit button dispatches edit-host event with host data
- Connect button links to /terminal?host={id} for pre-selection
- Terminal page reads selected host from query param
- Toast notification system via Alpine.js events
- Add default template function for fallback values
This commit is contained in:
swanadiva
2026-07-07 15:51:57 +07:00
parent 3df8416d9b
commit 301115b74d
8 changed files with 285 additions and 58 deletions
+92 -15
View File
@@ -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"),
})
}
+7 -5
View File
@@ -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)