feat: Sprint 3 — settings page with tabs/toggles/devices/danger-zone, brief page
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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' : '';
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Quick Brief · Hostkeeper V2</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/output.css">
|
||||
<link rel="stylesheet" href="/static/css/custom.css">
|
||||
<script src="/static/js/htmx.min.js"></script>
|
||||
<script src="/static/js/alpine.min.js" defer></script>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface antialiased dot-grid min-h-screen">
|
||||
<div class="flex min-h-screen">
|
||||
{{template "nav" .}}
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<header class="h-14 border-b border-outline-variant/30 flex items-center px-6 bg-white/70 backdrop-blur-md">
|
||||
<h1 class="text-lg font-semibold text-on-surface">{{.Title}}</h1>
|
||||
</header>
|
||||
<main id="main-content" class="flex-1 overflow-auto p-6">
|
||||
<div class="max-w-3xl mx-auto space-y-6">
|
||||
|
||||
<div class="rounded-xl bg-white border border-outline-variant/30 p-6">
|
||||
<h2 class="text-base font-semibold text-on-surface mb-3">Keyboard Shortcuts</h2>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex items-center justify-between py-1.5 border-b border-outline-variant/10 last:border-0">
|
||||
<span class="text-on-surface-variant">New Host</span>
|
||||
<kbd class="px-2 py-0.5 rounded bg-surface-container-low text-xs font-mono text-on-surface">Cmd + N</kbd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-1.5 border-b border-outline-variant/10 last:border-0">
|
||||
<span class="text-on-surface-variant">Search</span>
|
||||
<kbd class="px-2 py-0.5 rounded bg-surface-container-low text-xs font-mono text-on-surface">Cmd + K</kbd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-1.5 border-b border-outline-variant/10 last:border-0">
|
||||
<span class="text-on-surface-variant">Toggle Dark Mode</span>
|
||||
<kbd class="px-2 py-0.5 rounded bg-surface-container-low text-xs font-mono text-on-surface">Cmd + D</kbd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-1.5 border-b border-outline-variant/10 last:border-0">
|
||||
<span class="text-on-surface-variant">Quick Brief</span>
|
||||
<kbd class="px-2 py-0.5 rounded bg-surface-container-low text-xs font-mono text-on-surface">Cmd + B</kbd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-1.5 border-b border-outline-variant/10 last:border-0">
|
||||
<span class="text-on-surface-variant">Settings</span>
|
||||
<kbd class="px-2 py-0.5 rounded bg-surface-container-low text-xs font-mono text-on-surface">Cmd + ,</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-white border border-outline-variant/30 p-6">
|
||||
<h2 class="text-base font-semibold text-on-surface mb-3">About Hostkeeper V2</h2>
|
||||
<div class="space-y-2 text-sm text-on-surface-variant">
|
||||
<p>Hostkeeper V2 is a free, open-source Termius alternative for managing SSH/SFTP connections.</p>
|
||||
<p>Built with GoFiber v2 + HTMX + Alpine.js + TailwindCSS v4.</p>
|
||||
<p>Version: 2.0.0-dev</p>
|
||||
<p>License: MIT</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,222 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings · Hostkeeper V2</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/output.css">
|
||||
<link rel="stylesheet" href="/static/css/custom.css">
|
||||
<script src="/static/js/htmx.min.js"></script>
|
||||
<script src="/static/js/alpine.min.js" defer></script>
|
||||
<script src="/static/js/clipboard.js"></script>
|
||||
<script src="/static/js/utils.js"></script>
|
||||
<script src="/static/js/toggles.js"></script>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface antialiased dot-grid min-h-screen">
|
||||
<div class="flex min-h-screen">
|
||||
{{template "nav" .}}
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<header class="h-14 border-b border-outline-variant/30 flex items-center px-6 bg-white/70 backdrop-blur-md">
|
||||
<h1 class="text-lg font-semibold text-on-surface">{{.Title}}</h1>
|
||||
</header>
|
||||
<main id="main-content" class="flex-1 overflow-auto p-6">
|
||||
<div class="max-w-3xl mx-auto space-y-8 pb-16">
|
||||
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold text-on-surface mb-4">Profile</h2>
|
||||
<div class="bg-white rounded-xl border border-outline-variant/30 p-6 space-y-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="w-14 h-14 rounded-full bg-tertiary/20 flex items-center justify-center text-tertiary text-xl font-semibold">U</div>
|
||||
<div>
|
||||
<p class="text-base font-medium text-on-surface">User</p>
|
||||
<p class="text-sm text-on-surface-variant">Local administrator</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-on-surface mb-1">Display Name</label>
|
||||
<input type="text" value="User" readonly
|
||||
class="w-full px-3 py-2 rounded-lg border border-outline-variant/50 bg-surface-container-low/30 text-sm text-on-surface">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-on-surface mb-1">Username</label>
|
||||
<input type="text" value="admin" readonly
|
||||
class="w-full px-3 py-2 rounded-lg border border-outline-variant/50 bg-surface-container-low/30 text-sm text-on-surface">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-data="{ tab: 'appearance' }">
|
||||
<div class="flex gap-1 mb-4 bg-surface-container-low/50 rounded-lg p-1 w-fit">
|
||||
<button @click="tab = 'appearance'" :class="tab === 'appearance' ? 'bg-white shadow-sm text-on-surface' : 'text-on-surface-variant hover:text-on-surface'"
|
||||
class="px-4 py-1.5 rounded-md text-sm font-medium transition-colors">Appearance</button>
|
||||
<button @click="tab = 'terminal'" :class="tab === 'terminal' ? 'bg-white shadow-sm text-on-surface' : 'text-on-surface-variant hover:text-on-surface'"
|
||||
class="px-4 py-1.5 rounded-md text-sm font-medium transition-colors">Terminal</button>
|
||||
<button @click="tab = 'connection'" :class="tab === 'connection' ? 'bg-white shadow-sm text-on-surface' : 'text-on-surface-variant hover:text-on-surface'"
|
||||
class="px-4 py-1.5 rounded-md text-sm font-medium transition-colors">Connection</button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/settings" class="bg-white rounded-xl border border-outline-variant/30 p-6 space-y-5">
|
||||
<div x-show="tab === 'appearance'" x-cloak class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-on-surface mb-1">Theme</label>
|
||||
<select name="theme"
|
||||
class="w-full max-w-xs px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||
<option value="light" {{if eq .Config.Theme "light"}}selected{{end}}>Light</option>
|
||||
<option value="dark" {{if eq .Config.Theme "dark"}}selected{{end}}>Dark (coming soon)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-on-surface mb-1">Font Size</label>
|
||||
<select name="fontSize"
|
||||
class="w-full max-w-xs px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||
<option value="12" {{if eq .Config.FontSize 12}}selected{{end}}>12px</option>
|
||||
<option value="13" {{if eq .Config.FontSize 13}}selected{{end}}>13px</option>
|
||||
<option value="14" {{if eq .Config.FontSize 14}}selected{{end}}>14px</option>
|
||||
<option value="15" {{if eq .Config.FontSize 15}}selected{{end}}>15px</option>
|
||||
<option value="16" {{if eq .Config.FontSize 16}}selected{{end}}>16px</option>
|
||||
<option value="18" {{if eq .Config.FontSize 18}}selected{{end}}>18px</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="tab === 'terminal'" x-cloak class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-on-surface mb-1">Scrollback Lines</label>
|
||||
<select name="scrollback"
|
||||
class="w-full max-w-xs px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||
<option value="1000" {{if eq .Config.Scrollback 1000}}selected{{end}}>1,000</option>
|
||||
<option value="5000" {{if eq .Config.Scrollback 5000}}selected{{end}}>5,000</option>
|
||||
<option value="10000" {{if eq .Config.Scrollback 10000}}selected{{end}}>10,000</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="flex items-center justify-between py-2">
|
||||
<span class="text-sm text-on-surface">Blinking Cursor</span>
|
||||
<button type="button" role="switch"
|
||||
x-data="{ on: {{if .Config.BlinkCursor}}true{{else}}false{{end}} }"
|
||||
@click="on = !on; $el.classList.toggle('active'); $el.nextElementSibling.value = on ? 'on' : 'off'"
|
||||
:class="on ? 'active' : ''"
|
||||
class="toggle-switch" aria-checked="{{if .Config.BlinkCursor}}true{{else}}false{{end}}">
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
<input type="hidden" name="blinkCursor" value="{{if .Config.BlinkCursor}}on{{end}}">
|
||||
</label>
|
||||
<label class="flex items-center justify-between py-2">
|
||||
<span class="text-sm text-on-surface">Terminal Bell</span>
|
||||
<button type="button" role="switch"
|
||||
x-data="{ on: {{if .Config.BellEnabled}}true{{else}}false{{end}} }"
|
||||
@click="on = !on; $el.classList.toggle('active'); $el.nextElementSibling.value = on ? 'on' : 'off'"
|
||||
:class="on ? 'active' : ''"
|
||||
class="toggle-switch">
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
<input type="hidden" name="bellEnabled" value="{{if .Config.BellEnabled}}on{{end}}">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div x-show="tab === 'connection'" x-cloak class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-on-surface mb-1">Auto-Lock (minutes)</label>
|
||||
<select name="autoLock"
|
||||
class="w-full max-w-xs px-3 py-2 rounded-lg border border-outline-variant/50 bg-white text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30">
|
||||
<option value="1" {{if eq .Config.AutoLock 1}}selected{{end}}>1 min</option>
|
||||
<option value="5" {{if eq .Config.AutoLock 5}}selected{{end}}>5 min</option>
|
||||
<option value="15" {{if eq .Config.AutoLock 15}}selected{{end}}>15 min</option>
|
||||
<option value="30" {{if eq .Config.AutoLock 30}}selected{{end}}>30 min</option>
|
||||
<option value="0" {{if eq .Config.AutoLock 0}}selected{{end}}>Never</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="flex items-center justify-between py-2">
|
||||
<span class="text-sm text-on-surface">TCP Keepalive</span>
|
||||
<button type="button" role="switch"
|
||||
x-data="{ on: {{if .Config.Keepalive}}true{{else}}false{{end}} }"
|
||||
@click="on = !on; $el.classList.toggle('active'); $el.nextElementSibling.value = on ? 'on' : 'off'"
|
||||
:class="on ? 'active' : ''"
|
||||
class="toggle-switch">
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
<input type="hidden" name="keepalive" value="{{if .Config.Keepalive}}on{{end}}">
|
||||
</label>
|
||||
<label class="flex items-center justify-between py-2">
|
||||
<span class="text-sm text-on-surface">Copy on Select</span>
|
||||
<button type="button" role="switch"
|
||||
x-data="{ on: {{if .Config.CopyOnSelect}}true{{else}}false{{end}} }"
|
||||
@click="on = !on; $el.classList.toggle('active'); $el.nextElementSibling.value = on ? 'on' : 'off'"
|
||||
:class="on ? 'active' : ''"
|
||||
class="toggle-switch">
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
<input type="hidden" name="copyOnSelect" value="{{if .Config.CopyOnSelect}}on{{end}}">
|
||||
</label>
|
||||
<label class="flex items-center justify-between py-2">
|
||||
<span class="text-sm text-on-surface">Auto-Reconnect</span>
|
||||
<button type="button" role="switch"
|
||||
x-data="{ on: {{if .Config.AutoReconnect}}true{{else}}false{{end}} }"
|
||||
@click="on = !on; $el.classList.toggle('active'); $el.nextElementSibling.value = on ? 'on' : 'off'"
|
||||
:class="on ? 'active' : ''"
|
||||
class="toggle-switch">
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
<input type="hidden" name="autoReconnect" value="{{if .Config.AutoReconnect}}on{{end}}">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit"
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium bg-primary text-white hover:bg-primary/90 transition-colors">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold text-on-surface mb-4">Connected Devices</h2>
|
||||
<div class="bg-white rounded-xl border border-outline-variant/30 divide-y divide-outline-variant/20">
|
||||
{{range .Devices}}
|
||||
<div class="flex items-center gap-4 px-6 py-4">
|
||||
<div class="w-8 h-8 rounded-lg bg-tertiary/10 text-tertiary flex items-center justify-center shrink-0">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{{if eq .Type "desktop"}}<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>{{end}}
|
||||
{{if eq .Type "mobile"}}<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/>{{end}}
|
||||
{{if eq .Type "tablet"}}<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>{{end}}
|
||||
</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>
|
||||
{{if .Current}}<span class="text-xs font-medium px-1.5 py-0.5 rounded-full bg-secondary/10 text-secondary">Current</span>{{end}}
|
||||
</div>
|
||||
<p class="text-xs text-on-surface-variant">{{.OS}} · {{.LastSeen}}</p>
|
||||
</div>
|
||||
<button class="text-xs font-medium text-red-500 hover:text-red-600 transition-colors">Remove</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-data="{ confirm: false }">
|
||||
<h2 class="text-lg font-semibold text-red-600 mb-4">Danger Zone</h2>
|
||||
<div class="bg-white rounded-xl border border-red-200 p-6">
|
||||
<p class="text-sm text-on-surface-variant mb-4">This will permanently delete all your data, including hosts, keys, snippets, and settings. This action cannot be undone.</p>
|
||||
<div x-show="!confirm">
|
||||
<button @click="confirm = true"
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium bg-red-500 text-white hover:bg-red-600 transition-colors">Reset All Data</button>
|
||||
</div>
|
||||
<div x-show="confirm" x-cloak class="flex items-center gap-3">
|
||||
<p class="text-sm font-medium text-red-600">Are you sure?</p>
|
||||
<button @click="localStorage.clear(); confirm = false; alert('Data cleared')"
|
||||
class="px-4 py-2 rounded-lg text-sm font-medium bg-red-500 text-white hover:bg-red-600 transition-colors">Yes, Delete Everything</button>
|
||||
<button @click="confirm = 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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+11
-9
@@ -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)` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user