feat: Sprint 1 final — delete host, Alpine modal events, grid/list toggle, transfers panel

This commit is contained in:
swanadiva
2026-07-07 13:17:31 +07:00
parent d771925727
commit 86620b034f
7 changed files with 94 additions and 30 deletions
+11
View File
@@ -57,6 +57,17 @@ func CreateHost(c *fiber.Ctx) error {
})
}
func DeleteHost(c *fiber.Ctx) error {
id := c.Params("id")
store.Delete(id)
hosts := store.All()
return c.Render("host_list", fiber.Map{
"Hosts": hosts,
"Active": countByStatus(hosts, "active"),
"Offline": countByStatus(hosts, "offline"),
})
}
func countByStatus(hosts []model.Host, status string) int {
n := 0
for _, h := range hosts {
+9
View File
@@ -49,6 +49,15 @@ func (s *HostStore) Search(q, filter string) []Host {
return result
}
func (s *HostStore) Delete(id string) {
for i, h := range s.Hosts {
if h.ID == id {
s.Hosts = append(s.Hosts[:i], s.Hosts[i+1:]...)
return
}
}
}
func (s *HostStore) Add(name, ip, os, provider, hostType string) Host {
h := Host{
ID: randString(8),
+1
View File
@@ -31,6 +31,7 @@ func main() {
app.Get("/", handler.Dashboard)
app.Get("/hosts", handler.DashboardList)
app.Post("/hosts", handler.CreateHost)
app.Delete("/hosts/:id", handler.DeleteHost)
app.Get("/api/health", handler.Health)
log.Fatal(app.Listen(":1947"))
+3
View File
@@ -1,5 +1,8 @@
/* Lumina System — Custom CSS */
/* Alpine cloak — hide until initialized */
[x-cloak] { display: none !important; }
/* Dot grid background */
.dot-grid {
background-image: radial-gradient(#e2e8f0 1.5px, transparent 1.5px);
+57 -25
View File
@@ -1,29 +1,61 @@
{{define "host_list"}}
<div id="host-list" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{{range .Hosts}}
<div class="rounded-xl bg-white border border-outline-variant/30 p-4 hover:shadow-md transition-shadow animate-fade-in">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg {{if eq .Status "active"}}bg-primary/10 text-primary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}} flex items-center justify-center">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
<div id="host-list" class="flex flex-col gap-4">
<div x-show="view === 'grid'" x-cloak
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{{range .Hosts}}
<div class="rounded-xl bg-white border border-outline-variant/30 p-4 hover:shadow-md transition-shadow animate-fade-in">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg {{if eq .Status "active"}}bg-primary/10 text-primary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}} flex items-center justify-center">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
</div>
<div>
<p class="text-sm font-medium text-on-surface">{{.Name}}</p>
<p class="text-xs text-on-surface-variant">{{.IP}}</p>
</div>
</div>
<div>
<p class="text-sm font-medium text-on-surface">{{.Name}}</p>
<p class="text-xs text-on-surface-variant">{{.IP}}</p>
<span class="text-xs font-medium px-2 py-0.5 rounded-full {{if eq .Status "active"}}bg-secondary/10 text-secondary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}}">
{{.Status}}
</span>
</div>
<div class="flex items-center gap-2 text-xs text-on-surface-variant mb-3">
<span>{{.OS}}</span>
<span>·</span>
<span>{{.Provider}}</span>
</div>
<div class="flex items-center justify-between pt-3 border-t border-outline-variant/20">
<span class="text-xs text-on-surface-variant">{{.LastSeen}}</span>
<div class="flex gap-1">
<button class="p-1.5 rounded-lg hover:bg-primary/10 text-on-surface-variant hover:text-primary transition-colors"
onclick="alert('Connect to {{.Name}}')" title="Connect">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
</button>
<button class="p-1.5 rounded-lg hover:bg-red-50 text-on-surface-variant hover:text-red-500 transition-colors"
hx-delete="/hosts/{{.ID}}" hx-target="#host-list" hx-confirm="Delete {{.Name}}?" title="Delete">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</div>
<span class="text-xs font-medium px-2 py-0.5 rounded-full {{if eq .Status "active"}}bg-secondary/10 text-secondary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}}">
{{.Status}}
</span>
</div>
<div class="flex items-center gap-2 text-xs text-on-surface-variant mb-3">
<span>{{.OS}}</span>
<span>·</span>
<span>{{.Provider}}</span>
</div>
<div class="flex items-center justify-between pt-3 border-t border-outline-variant/20">
<span class="text-xs text-on-surface-variant">{{.LastSeen}}</span>
<div class="flex gap-1">
{{end}}
</div>
<div x-show="view === 'list'" x-cloak class="flex flex-col gap-2">
{{range .Hosts}}
<div class="rounded-xl bg-white border border-outline-variant/30 px-4 py-3 flex items-center gap-4 hover:shadow-sm transition-shadow animate-fade-in">
<div class="w-2 h-2 rounded-full {{if eq .Status "active"}}bg-secondary{{else}}bg-on-surface-variant{{end}} shrink-0"></div>
<div class="w-8 h-8 rounded-lg {{if eq .Status "active"}}bg-primary/10 text-primary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}} flex items-center justify-center shrink-0">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<p class="text-sm font-medium text-on-surface">{{.Name}}</p>
<span class="text-xs font-medium px-1.5 py-0.5 rounded-full {{if eq .Status "active"}}bg-secondary/10 text-secondary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}}">{{.Status}}</span>
</div>
<p class="text-xs text-on-surface-variant truncate">{{.IP}} · {{.OS}} · {{.Provider}}</p>
</div>
<span class="text-xs text-on-surface-variant shrink-0">{{.LastSeen}}</span>
<div class="flex gap-1 shrink-0">
<button class="p-1.5 rounded-lg hover:bg-primary/10 text-on-surface-variant hover:text-primary transition-colors"
onclick="alert('Connect to {{.Name}}')" title="Connect">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
@@ -34,20 +66,20 @@
</button>
</div>
</div>
{{end}}
</div>
{{end}}
{{if eq (len .Hosts) 0}}
<div class="col-span-full text-center py-12 text-on-surface-variant">
<div class="text-center py-12 text-on-surface-variant">
<p class="text-lg font-medium">No hosts found</p>
<p class="text-sm mt-1">Try a different search or add a new host</p>
</div>
{{end}}
<div class="rounded-xl border-2 border-dashed border-outline-variant/30 p-6 flex flex-col items-center justify-center text-on-surface-variant/50 hover:border-primary/30 hover:text-primary/50 transition-colors cursor-pointer min-h-[160px]"
@click="document.getElementById('host-modal').classList.remove('hidden')">
@click="$dispatch('open-modal')">
<svg class="w-8 h-8 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
<span class="text-sm font-medium">Add New Host</span>
</div>
</div>
{{end}}
{{end}}
+6 -5
View File
@@ -1,18 +1,19 @@
{{define "host_modal"}}
<div id="host-modal" class="fixed inset-0 z-50 hidden" x-data="{ open: false }" x-init="open = true">
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="open = false; $el.parentElement.classList.add('hidden')"></div>
<div id="host-modal" class="fixed inset-0 z-50" x-data="{ open: false }" x-show="open" x-cloak
@open-modal.window="open = true" @keydown.escape.window="open = false">
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="open = false"></div>
<div class="absolute inset-0 flex items-center justify-center p-4" x-show="open" x-transition>
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6" @click.outside="open = false">
<div class="flex items-center justify-between mb-6">
<h2 class="text-lg font-semibold text-on-surface">Add New Host</h2>
<button @click="open = false; $el.closest('#host-modal').classList.add('hidden')"
<button @click="open = false"
class="p-1 rounded-lg hover:bg-surface-container-low text-on-surface-variant">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
<form hx-post="/hosts" hx-target="#host-list" hx-swap="outerHTML"
@submit="open = false; setTimeout(() => document.getElementById('host-modal').classList.add('hidden'), 100)"
@submit="open = false"
class="space-y-4">
<div>
<label class="block text-sm font-medium text-on-surface mb-1">Host Name</label>
@@ -60,7 +61,7 @@
</select>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" @click="open = false; $el.closest('#host-modal').classList.add('hidden')"
<button type="button" @click="open = false"
class="px-4 py-2 rounded-lg text-sm font-medium text-on-surface-variant hover:bg-surface-container-low transition-colors">
Cancel
</button>
+7
View File
@@ -18,6 +18,13 @@
</a>
{{end}}
</div>
<div class="px-3 py-3 border-t border-outline-variant/20">
<p class="text-xs font-medium text-on-surface-variant uppercase tracking-wide mb-2 px-3">Transfers</p>
<div class="px-3 py-2 rounded-lg bg-surface-container-low/50 text-xs text-on-surface-variant flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-on-surface-variant/30"></span>
No active transfers
</div>
</div>
<div class="p-4 border-t border-outline-variant/20">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-tertiary/20 flex items-center justify-center text-tertiary text-sm font-semibold">U</div>