diff --git a/AGENTS.md b/AGENTS.md index 6886f7d..9bdf4ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ > **Purpose**: Memungkinkan AI agent berikutnya melanjutkan development tanpa mengulang atau merusak. > **Last Updated**: 2026-07-07 -> **Current Phase**: Sprint 1 + 2 selesai, Sprint 3 berikutnya +> **Current Phase**: Sprint 1 + 2 + 3 selesai, Sprint 4 berikutnya --- @@ -115,7 +115,7 @@ Alasan: ### ❌ Belum Dimulai / In Progress - **Sprint 2 (DONE)**: Snippets + Keychain -- **Sprint 3 (NEXT)**: Settings + Brief +- **Sprint 3 (DONE)**: Settings + Brief - Sprint 4: SFTP Dual-Pane - Sprint 5: Terminal + xterm.js - Sprint 6: Storage Integration diff --git a/app/backend/internal/handler/brief.go b/app/backend/internal/handler/brief.go new file mode 100644 index 0000000..5aec9bc --- /dev/null +++ b/app/backend/internal/handler/brief.go @@ -0,0 +1,11 @@ +package handler + +import "github.com/gofiber/fiber/v2" + +func Brief(c *fiber.Ctx) error { + return c.Render("brief", fiber.Map{ + "View": "brief", + "Title": "Quick Brief", + "NavItems": navItems, + }) +} diff --git a/app/backend/internal/handler/settings.go b/app/backend/internal/handler/settings.go new file mode 100644 index 0000000..6ea6415 --- /dev/null +++ b/app/backend/internal/handler/settings.go @@ -0,0 +1,44 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model" +) + +var deviceStore = model.NewDeviceStore() +var configStore = model.NewConfigStore() + +func Settings(c *fiber.Ctx) error { + return c.Render("settings", fiber.Map{ + "View": "settings", + "Title": "Settings", + "Config": configStore.Get(), + "Devices": deviceStore.All(), + "NavItems": navItems, + }) +} + +func UpdateConfig(c *fiber.Ctx) error { + cfg := configStore.Get() + if v := c.FormValue("theme"); v != "" { cfg.Theme = v } + if v := c.FormValue("fontSize"); v != "" { cfg.FontSize = parseInt(v, 14) } + if v := c.FormValue("scrollback"); v != "" { cfg.Scrollback = parseInt(v, 5000) } + if v := c.FormValue("autoLock"); v != "" { cfg.AutoLock = parseInt(v, 5) } + cfg.Keepalive = c.FormValue("keepalive") == "on" + cfg.CopyOnSelect = c.FormValue("copyOnSelect") == "on" + cfg.BellEnabled = c.FormValue("bellEnabled") == "on" + cfg.AutoReconnect = c.FormValue("autoReconnect") == "on" + cfg.BlinkCursor = c.FormValue("blinkCursor") == "on" + configStore.Update(cfg) + return c.Redirect("/settings", 303) +} + +func parseInt(s string, def int) int { + i := 0 + for _, c := range s { + if c < '0' || c > '9' { return def } + i = i*10 + int(c-'0') + } + if i == 0 { return def } + return i +} diff --git a/app/backend/internal/model/config.go b/app/backend/internal/model/config.go new file mode 100644 index 0000000..25ce594 --- /dev/null +++ b/app/backend/internal/model/config.go @@ -0,0 +1,39 @@ +package model + +type AppConfig struct { + Theme string `json:"theme"` // light | dark + FontSize int `json:"fontSize"` // 12-18 + Scrollback int `json:"scrollback"` // 1000-10000 + AutoLock int `json:"autoLock"` // minutes + Keepalive bool `json:"keepalive"` + CopyOnSelect bool `json:"copyOnSelect"` + BellEnabled bool `json:"bellEnabled"` + AutoReconnect bool `json:"autoReconnect"` + BlinkCursor bool `json:"blinkCursor"` +} + +type ConfigStore struct { + Config AppConfig +} + +func NewConfigStore() *ConfigStore { + return &ConfigStore{Config: defaultConfig()} +} + +func (s *ConfigStore) Get() AppConfig { return s.Config } + +func (s *ConfigStore) Update(cfg AppConfig) { s.Config = cfg } + +func defaultConfig() AppConfig { + return AppConfig{ + Theme: "light", + FontSize: 14, + Scrollback: 5000, + AutoLock: 5, + Keepalive: true, + CopyOnSelect: true, + BellEnabled: false, + AutoReconnect: true, + BlinkCursor: false, + } +} diff --git a/app/backend/internal/model/device.go b/app/backend/internal/model/device.go new file mode 100644 index 0000000..d195a58 --- /dev/null +++ b/app/backend/internal/model/device.go @@ -0,0 +1,28 @@ +package model + +type Device struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` // desktop | mobile | tablet + OS string `json:"os"` + LastSeen string `json:"lastSeen"` + Current bool `json:"current"` +} + +type DeviceStore struct { + Devices []Device +} + +func NewDeviceStore() *DeviceStore { + return &DeviceStore{Devices: defaultDevices()} +} + +func (s *DeviceStore) All() []Device { return s.Devices } + +func defaultDevices() []Device { + return []Device{ + {ID: "d1", Name: "MacBook Pro M4", Type: "desktop", OS: "macOS 15 Sequoia", LastSeen: "Just now", Current: true}, + {ID: "d2", Name: "iPhone 17 Pro", Type: "mobile", OS: "iOS 20", LastSeen: "2 hours ago", Current: false}, + {ID: "d3", Name: "iPad Air M3", Type: "tablet", OS: "iPadOS 20", LastSeen: "Yesterday", Current: false}, + } +} diff --git a/app/backend/main.go b/app/backend/main.go index f1b6a63..00c40d9 100644 --- a/app/backend/main.go +++ b/app/backend/main.go @@ -40,6 +40,9 @@ func main() { app.Get("/keys/grid", handler.KeyGrid) app.Post("/keys", handler.CreateKey) app.Delete("/keys/:id", handler.DeleteKey) + app.Get("/settings", handler.Settings) + app.Post("/settings", handler.UpdateConfig) + app.Get("/brief", handler.Brief) app.Get("/api/health", handler.Health) log.Fatal(app.Listen(":1947")) diff --git a/app/backend/static/js/toggles.js b/app/backend/static/js/toggles.js new file mode 100644 index 0000000..d6be451 --- /dev/null +++ b/app/backend/static/js/toggles.js @@ -0,0 +1,13 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('switchToggle', (initial) => ({ + on: initial, + toggle() { + this.on = !this.on; + this.$el.classList.toggle('active', this.on); + this.$nextTick(() => { + const hidden = this.$el.nextElementSibling; + if (hidden) hidden.value = this.on ? 'on' : ''; + }); + } + })); +}); diff --git a/app/backend/views/brief.html b/app/backend/views/brief.html new file mode 100644 index 0000000..f234a46 --- /dev/null +++ b/app/backend/views/brief.html @@ -0,0 +1,65 @@ + + + + + + Quick Brief · Hostkeeper V2 + + + + + + + + +
+ {{template "nav" .}} +
+
+

{{.Title}}

+
+
+
+ +
+

Keyboard Shortcuts

+
+
+ New Host + Cmd + N +
+
+ Search + Cmd + K +
+
+ Toggle Dark Mode + Cmd + D +
+
+ Quick Brief + Cmd + B +
+
+ Settings + Cmd + , +
+
+
+ +
+

About Hostkeeper V2

+
+

Hostkeeper V2 is a free, open-source Termius alternative for managing SSH/SFTP connections.

+

Built with GoFiber v2 + HTMX + Alpine.js + TailwindCSS v4.

+

Version: 2.0.0-dev

+

License: MIT

+
+
+ +
+
+
+
+ + diff --git a/app/backend/views/settings.html b/app/backend/views/settings.html new file mode 100644 index 0000000..e67037e --- /dev/null +++ b/app/backend/views/settings.html @@ -0,0 +1,222 @@ + + + + + + Settings · Hostkeeper V2 + + + + + + + + + + + +
+ {{template "nav" .}} +
+
+

{{.Title}}

+
+
+
+ +
+

Profile

+
+
+
U
+
+

User

+

Local administrator

+
+
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+ + + +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ + +
+ +
+
+ + +
+ + + +
+ +
+ +
+
+
+ +
+

Connected Devices

+
+ {{range .Devices}} +
+
+ + {{if eq .Type "desktop"}}{{end}} + {{if eq .Type "mobile"}}{{end}} + {{if eq .Type "tablet"}}{{end}} + +
+
+
+

{{.Name}}

+ {{if .Current}}Current{{end}} +
+

{{.OS}} · {{.LastSeen}}

+
+ +
+ {{end}} +
+
+ +
+

Danger Zone

+
+

This will permanently delete all your data, including hosts, keys, snippets, and settings. This action cannot be undone.

+
+ +
+
+

Are you sure?

+ + +
+
+
+ +
+
+
+
+ + diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 2c5a323..17435bd 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -37,7 +37,7 @@ Lihat [ARCHITECTURE_HTMX.md](ARCHITECTURE_HTMX.md) untuk detail. | Sprint 0 — Setup GoFiber + HTMX | DONE | | Sprint 1 — Dashboard + Host List | DONE | | Sprint 2 — Snippets + Keychain | DONE | -| Sprint 3 — Settings + Brief | NOT STARTED | +| Sprint 3 — Settings + Brief | DONE | | Sprint 4 — SFTP Dual-Pane | NOT STARTED | | Sprint 5 — Terminal + xterm.js | NOT STARTED | | Sprint 6 — Storage Integration | NOT STARTED | @@ -89,13 +89,14 @@ Lihat [ARCHITECTURE_HTMX.md](ARCHITECTURE_HTMX.md) untuk detail. - **Verify**: Both pages render, CRUD works, clipboard copies, strength badges display ### Sprint 3 — Settings + Brief -- [ ] Model Device + Config -- [ ] Settings handler -- [ ] Brief handler -- [ ] Settings template -- [ ] Brief template -- [ ] Toggle switch JS -- [ ] Danger zone +- [x] Model Device + Config (`internal/model/device.go`, `internal/model/config.go`) +- [x] Settings handler + UpdateConfig (`internal/handler/settings.go`) +- [x] Brief handler (`internal/handler/brief.go`) +- [x] Settings template (`views/settings.html`) — profile, tabs, toggles, devices, danger zone +- [x] Brief template (`views/brief.html`) — keyboard shortcuts, about +- [x] Toggle switch JS (`static/js/toggles.js`) — Alpine toggle data +- [x] Danger zone — confirm flow with localStorage.clear() +- **Verify**: Settings page renders with tabs, toggles work via Alpine, brief page shows shortcuts ### Sprint 4 — SFTP Dual-Pane - [ ] Model FileItem + Transfer @@ -142,7 +143,8 @@ Lihat [ARCHITECTURE_HTMX.md](ARCHITECTURE_HTMX.md) untuk detail. | 2026-07-07 | Sprint 0: GoFiber+HTMX scaffolding, TailwindCSS, layout, nav, health | `43a19b7` | | 2026-07-07 | Sprint 1: Host model+store, dashboard CRUD, HTMX partials (grid/list, search, filter, modal, delete), transfers panel | `d771925`, `86620b0` | | 2026-07-07 | Sprint 2 dimulai: Snippet + Keychain models created | — | -| 2026-07-07 | Sprint 2 selesai: all CRUD, standalone templates, clipboard.js, utils.js, fix template {{define}} conflict | `(pending)` | +| 2026-07-07 | Sprint 2 selesai: all CRUD, standalone templates, clipboard.js, utils.js, fix template {{define}} conflict | `c4b0d7d` | +| 2026-07-07 | Sprint 3 selesai: settings page, tabs, toggles, connected devices, danger zone, brief page | `(pending)` | ---