fix: UI overhaul — align all HTMX views with Lumina Design System (46 fixes)
- All 7 pages rewritten: Dashboard, Terminal, Snippets, Keychain, SFTP, Settings, Brief - Apply consistent Lumina glassmorphic theme (rounded-2xl, shadows, borders, tracking) - Fix SVG icons not rendering — return template.HTML instead of string - Fix Alpine x-data scoping: terminal tabs, SFTP modals, snippet tag buttons - Fix dashboard search sending literal ?q=... - Rebuild CSS with all new Tailwind classes (62KB) - Add .air.toml with hot-reload config, docs/PERBAIKANUI.md checklist - Remove unused backup partials
This commit is contained in:
@@ -3,9 +3,13 @@ package handler
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
|
||||
func Brief(c *fiber.Ctx) error {
|
||||
return c.Render("brief", fiber.Map{
|
||||
data := fiber.Map{
|
||||
"View": "brief",
|
||||
"Title": "Quick Brief",
|
||||
"NavItems": navItems,
|
||||
})
|
||||
}
|
||||
if c.Get("HX-Request") != "" {
|
||||
return c.Render("brief_content", data)
|
||||
}
|
||||
return c.Render("brief", data)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
var keyStore = store.NewKeyStore()
|
||||
|
||||
func Keychain(c *fiber.Ctx) error {
|
||||
if c.Get("HX-Request") != "" {
|
||||
return KeyGrid(c)
|
||||
}
|
||||
keys := keyStore.All()
|
||||
return c.Render("keychain", fiber.Map{
|
||||
"View": "keychain",
|
||||
|
||||
@@ -10,13 +10,17 @@ var deviceStore = model.NewDeviceStore()
|
||||
var settingsStore = store.NewSettingsStore()
|
||||
|
||||
func Settings(c *fiber.Ctx) error {
|
||||
return c.Render("settings", fiber.Map{
|
||||
data := fiber.Map{
|
||||
"View": "settings",
|
||||
"Title": "Settings",
|
||||
"Config": settingsStore.Get(),
|
||||
"Devices": deviceStore.All(),
|
||||
"NavItems": navItems,
|
||||
})
|
||||
}
|
||||
if c.Get("HX-Request") != "" {
|
||||
return c.Render("settings_content", data)
|
||||
}
|
||||
return c.Render("settings", data)
|
||||
}
|
||||
|
||||
func UpdateConfig(c *fiber.Ctx) error {
|
||||
|
||||
@@ -9,7 +9,7 @@ var fileStore = model.NewFileStore()
|
||||
|
||||
func SFTP(c *fiber.Ctx) error {
|
||||
local, remote := fileStore.All()
|
||||
return c.Render("sftp", fiber.Map{
|
||||
data := fiber.Map{
|
||||
"View": "sftp",
|
||||
"Title": "SFTP",
|
||||
"LocalFiles": local,
|
||||
@@ -17,7 +17,11 @@ func SFTP(c *fiber.Ctx) error {
|
||||
"LocalBreadcrumb": fileStore.LocalBreadcrumb,
|
||||
"RemoteBreadcrumb": fileStore.RemoteBreadcrumb,
|
||||
"NavItems": navItems,
|
||||
})
|
||||
}
|
||||
if c.Get("HX-Request") != "" {
|
||||
return c.Render("sftp_content", data)
|
||||
}
|
||||
return c.Render("sftp", data)
|
||||
}
|
||||
|
||||
func SFTPPane(c *fiber.Ctx) error {
|
||||
|
||||
@@ -1,40 +1,88 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model"
|
||||
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/store"
|
||||
)
|
||||
|
||||
func jsonOrEmpty(v interface{}) string {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "[]"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func allTags(snippets []model.Snippet) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, sn := range snippets {
|
||||
for _, t := range sn.Tags {
|
||||
if !seen[t] {
|
||||
seen[t] = true
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var snippetStore = store.NewSnippetStore()
|
||||
|
||||
func Snippets(c *fiber.Ctx) error {
|
||||
if c.Get("HX-Request") != "" {
|
||||
return SnippetGrid(c)
|
||||
}
|
||||
snippets := snippetStore.All()
|
||||
return c.Render("snippets", fiber.Map{
|
||||
"View": "snippets",
|
||||
"Title": "Snippets",
|
||||
"Snippets": snippets,
|
||||
"NavItems": navItems,
|
||||
"View": "snippets",
|
||||
"Title": "Snippets",
|
||||
"Snippets": snippets,
|
||||
"SnippetsJSON": jsonOrEmpty(snippets),
|
||||
"AllTags": allTags(snippets),
|
||||
"NavItems": navItems,
|
||||
})
|
||||
}
|
||||
|
||||
func SnippetGrid(c *fiber.Ctx) error {
|
||||
q := c.Query("q", "")
|
||||
snippets := snippetStore.Search(q)
|
||||
return c.Render("snippet_grid", fiber.Map{"Snippets": snippets})
|
||||
collection := c.Query("collection", "")
|
||||
tag := c.Query("tag", "")
|
||||
snippets := snippetStore.Search(q, collection, tag)
|
||||
return c.Render("snippet_grid", fiber.Map{
|
||||
"Snippets": snippets,
|
||||
"SnippetsJSON": jsonOrEmpty(snippets),
|
||||
})
|
||||
}
|
||||
|
||||
func CreateSnippet(c *fiber.Ctx) error {
|
||||
name := c.FormValue("name")
|
||||
content := c.FormValue("content")
|
||||
language := c.FormValue("language")
|
||||
snippetStore.Add(name, content, language, nil)
|
||||
description := c.FormValue("description")
|
||||
collection := c.FormValue("collection")
|
||||
tagStr := c.FormValue("tags")
|
||||
var tags []string
|
||||
if tagStr != "" {
|
||||
tags = []string{tagStr}
|
||||
}
|
||||
snippetStore.Add(name, content, language, tags, description, collection)
|
||||
snippets := snippetStore.All()
|
||||
return c.Render("snippet_grid", fiber.Map{"Snippets": snippets})
|
||||
return c.Render("snippet_grid", fiber.Map{
|
||||
"Snippets": snippets,
|
||||
"SnippetsJSON": jsonOrEmpty(snippets),
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteSnippet(c *fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
snippetStore.Delete(id)
|
||||
snippets := snippetStore.All()
|
||||
return c.Render("snippet_grid", fiber.Map{"Snippets": snippets})
|
||||
return c.Render("snippet_grid", fiber.Map{
|
||||
"Snippets": snippets,
|
||||
"SnippetsJSON": jsonOrEmpty(snippets),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,13 +28,17 @@ func Terminal(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
hostsJSON, _ := json.Marshal(available)
|
||||
return c.Render("terminal", fiber.Map{
|
||||
data := fiber.Map{
|
||||
"View": "terminal",
|
||||
"Title": "Terminal",
|
||||
"NavItems": navItems,
|
||||
"Hosts": available,
|
||||
"HostsJSON": string(hostsJSON),
|
||||
})
|
||||
}
|
||||
if c.Get("HX-Request") != "" {
|
||||
return c.Render("terminal_content", data)
|
||||
}
|
||||
return c.Render("terminal", data)
|
||||
}
|
||||
|
||||
func TerminalWS(c *websocket.Conn) {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package model
|
||||
|
||||
type Key struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
URL string `json:"url"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Strength string `json:"strength"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Strength string `json:"strength"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
ConnectedHosts []string `json:"connectedHosts"`
|
||||
KeyFile string `json:"keyFile"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package model
|
||||
|
||||
type Snippet struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Language string `json:"language"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Language string `json:"language"`
|
||||
Description string `json:"description"`
|
||||
Collection string `json:"collection"`
|
||||
Icon string `json:"icon"`
|
||||
Tags []string `json:"tags"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
@@ -44,13 +44,16 @@ func (s *KeyStore) Search(q string) []model.Key {
|
||||
func (s *KeyStore) Add(name, username, url, passphrase string) model.Key {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
keyType := guessKeyType(name, passphrase)
|
||||
k := model.Key{
|
||||
ID: randID(),
|
||||
Name: name,
|
||||
Username: username,
|
||||
URL: url,
|
||||
Passphrase: passphrase,
|
||||
Strength: calcStrength(passphrase),
|
||||
Type: keyType,
|
||||
URL: url,
|
||||
KeyFile: "— " + keyType + " credential —",
|
||||
CreatedAt: time.Now().Format("Jan 2, 2006"),
|
||||
}
|
||||
s.keys = append(s.keys, k)
|
||||
@@ -58,6 +61,21 @@ func (s *KeyStore) Add(name, username, url, passphrase string) model.Key {
|
||||
return k
|
||||
}
|
||||
|
||||
func guessKeyType(name, passphrase string) string {
|
||||
switch {
|
||||
case len(passphrase) >= 25:
|
||||
return "ED25519"
|
||||
case contains(name, "aws") || contains(name, "AWS"):
|
||||
return "AWS Key"
|
||||
case contains(name, "token") || contains(name, "bearer") || contains(name, "TOKEN"):
|
||||
return "Bearer Token"
|
||||
case len(passphrase) <= 3:
|
||||
return "PASSWORD"
|
||||
default:
|
||||
return "SSH RSA"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *KeyStore) Delete(id string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
@@ -26,31 +26,47 @@ func (s *SnippetStore) All() []model.Snippet {
|
||||
return s.snippets
|
||||
}
|
||||
|
||||
func (s *SnippetStore) Search(q string) []model.Snippet {
|
||||
func (s *SnippetStore) Search(q, collection, tag string) []model.Snippet {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
if q == "" {
|
||||
return s.snippets
|
||||
}
|
||||
var res []model.Snippet
|
||||
for _, sn := range s.snippets {
|
||||
if contains(sn.Name, q) || contains(sn.Content, q) || contains(sn.Language, q) {
|
||||
res = append(res, sn)
|
||||
if q != "" && !contains(sn.Name, q) && !contains(sn.Content, q) && !contains(sn.Language, q) {
|
||||
continue
|
||||
}
|
||||
if collection != "" && sn.Collection != collection {
|
||||
continue
|
||||
}
|
||||
if tag != "" {
|
||||
found := false
|
||||
for _, t := range sn.Tags {
|
||||
if t == tag {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
res = append(res, sn)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *SnippetStore) Add(name, content, language string, tags []string) model.Snippet {
|
||||
func (s *SnippetStore) Add(name, content, language string, tags []string, description, collection string) model.Snippet {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
sn := model.Snippet{
|
||||
ID: randID(),
|
||||
Name: name,
|
||||
Content: content,
|
||||
Language: language,
|
||||
Tags: tags,
|
||||
CreatedAt: time.Now().Format("Jan 2, 2006"),
|
||||
ID: randID(),
|
||||
Name: name,
|
||||
Content: content,
|
||||
Language: language,
|
||||
Description: description,
|
||||
Collection: collection,
|
||||
Icon: "FileText",
|
||||
Tags: tags,
|
||||
CreatedAt: time.Now().Format("Jan 2, 2006"),
|
||||
}
|
||||
s.snippets = append(s.snippets, sn)
|
||||
writeFile("snippets.json", &s.snippets)
|
||||
|
||||
Reference in New Issue
Block a user