a0c8483c0e
- New ARCHITECTURE_HTMX.md — comprehensive HTMX architecture doc - ARCHITECTURE.md — updated to point to HTMX version - UI_COMPONENTS.md — replaced React tree with HTMX partials - SPRINT_PLAN.md — all sprint tasks updated for HTMX - BUILD_SYSTEM.md — simplified (no Vite, no npm) - TIMELINE.md — 3.5 week HTMX timeline - PROGRESS.md — updated status + HTMX - AGENTS.md — updated checkpoint with HTMX decision
15 KiB
15 KiB
Hostkeeper V2 — Architecture (HTMX)
Status: Approved — Menggantikan arsitektur React sebelumnya Last Updated: 2026-07-07 Keputusan: React → GoFiber + HTMX + Alpine.js + minimal JS
1. Arsitektur Baru
Hostkeeper V2 menggunakan arsitektur server-rendered HTML dengan interaktivitas via HTMX, bukan SPA React.
┌────────────────────────────────────────────────────┐
│ Browser / Electron WebView │
│ ┌──────────────────────────────────────────────┐ │
│ │ HTMX (14kb) + Alpine.js (15kb) + xterm.js │ │
│ │ │ │
│ │ ● HTMX: semua interaksi (search, filter, │ │
│ │ modal, form, navigasi) │ │
│ │ ● Alpine.js: client state (toast, toggle, │ │
│ │ passphrase reveal, tab) │ │
│ │ ● xterm.js: terminal emulator (hanya │ │
│ │ halaman terminal) │ │
│ │ ● Vanilla JS: clipboard, toggle switch, │ │
│ │ strength calc │ │
│ └────────────────┬─────────────────────────────┘ │
│ │ hx-get/hx-post/hx-ws │
│ ┌────────────────▼─────────────────────────────┐ │
│ │ GoFiber v2 Backend (localhost:1947) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ HTML handlers │ │ WebSocket /xterm.js │ │ │
│ │ │ (Fiber + html│ │ (SSH I/O via WS) │ │ │
│ │ │ template) │ └──────────────────────┘ │ │
│ │ └──────┬───────┘ │ │
│ │ │ go.mod replace │ │
│ │ ┌────▼────┐ │ │
│ │ │ v1/pkg/ │ ssh, crypto, storage, config │ │
│ │ └─────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Alur Request
1. User klik tombol "Filter Active"
│ hx-get="/hosts?filter=active"
│ hx-target="#host-list"
▼
2. GoFiber handler:
func (h *Handler) ListHosts(c fiber.Ctx) error {
hosts := h.store.Filter("active")
return c.Render("partials/host_list", fiber.Map{
"Hosts": hosts,
})
}
▼
3. Server return HTML partial
<div id="host-list" hx-swap-oob="true">
{{range .Hosts}}
<div class="host-card">...</div>
{{end}}
</div>
▼
4. HTMX swap HTML ke DOM (tanpa reload)
2. Tech Stack
| Layer | Teknologi | Versi | Fungsi |
|---|---|---|---|
| Backend | GoFiber v2 | v2.52+ | HTTP server, routing, middleware |
| Template | html/template | stdlib | Server-side HTML rendering |
| Frontend | HTMX | v2.0+ | HTML-over-HTTP interaktivitas |
| Client state | Alpine.js | v3.14+ | Modal, toast, toggle state |
| Terminal | xterm.js | v5.5+ | SSH terminal emulator |
| Styling | TailwindCSS v4 | v4.0+ | Utility-first CSS |
| CSS custom | Custom CSS | — | dot-grid, glassmorphism, keyframes |
| Icons | Lucide (inline SVG) | — | SVG icon system |
| Desktop | Electron (opsional) | v35+ | Browser wrapper |
| Mobile | WebView | — | Android/iOS shell |
3. Struktur Folder
app/backend/
├── main.go ← Entry point Fiber app
├── go.mod ← module git.tukangketik.id/swanadiva/hostkeeper/v2
├── go.sum
│
├── internal/
│ ├── handler/ ← HTTP handlers (render HTML + HTMX partials)
│ │ ├── dashboard.go GET/POST /hosts, /hosts/{id}
│ │ ├── terminal.go GET /terminal, WS /terminal/ws
│ │ ├── sftp.go GET /sftp/ls, POST /sftp/{action}
│ │ ├── snippets.go GET/POST /snippets
│ │ ├── keychain.go GET/POST /keys
│ │ ├── settings.go GET/POST /settings
│ │ ├── brief.go GET /brief (overlay)
│ │ └── health.go GET /api/health
│ │
│ ├── model/ ← Go structs (data models)
│ │ ├── host.go Host, HostFilter
│ │ ├── file.go FileItem
│ │ ├── snippet.go Snippet
│ │ ├── keychain.go KeychainItem
│ │ ├── device.go Device
│ │ ├── transfer.go BackgroundTransfer
│ │ └── config.go Config, Toggle
│ │
│ ├── template/ ← Template helpers/funcs
│ │ └── funcs.go activeClass, formatBytes, timeAgo, strengthBadge
│ │
│ └── middleware/
│ └── viewdata.go Inject .View, .ActiveNav, .User ke semua template
│
├── static/ ← Static assets (served via Fiber)
│ ├── css/
│ │ ├── output.css ← TailwindCSS v4 build output
│ │ └── custom.css ← dot-grid, glassmorphism, keyframes, scrollbar
│ ├── js/
│ │ ├── htmx.min.js ← HTMX (14kb)
│ │ ├── alpine.min.js ← Alpine.js (15kb)
│ │ ├── terminal.js ← xterm.js + WebSocket init
│ │ ├── clipboard.js ← navigator.clipboard.writeText()
│ │ ├── toggles.js ← Custom toggle switch click handler
│ │ └── utils.js ← passphrase reveal, strength calc, toast
│ ├── xterm/ ← xterm.js build files
│ └── img/ ← Images/icons
│
└── views/ ← Go HTML templates
├── layout.html ← Base layout (sidebar, header, scripts)
├── index.html ← Entry page (extends layout + content block)
│
└── partials/ ← HTMX-swappable partials
├── nav.html ← Sidebar navigation
├── dashboard.html ← Host page container
├── host_list.html ← Host card grid/list (HTMX swap target)
├── host_card.html ← Single host card
├── host_modal.html ← Add host modal form
├── terminal.html ← Terminal page (xterm.js container)
├── sftp.html ← Dual-pane file browser
├── sftp_pane.html ← Single file pane (for HTMX swap)
├── snippets.html ← Snippet page
├── snippet_grid.html ← Snippet cards (HTMX swap target)
├── snippet_modal.html ← Add snippet form
├── keychain.html ← Key page
├── key_grid.html ← Key cards (HTMX swap target)
├── key_modal.html ← Add key form
├── settings.html ← Settings form + toggles
├── brief.html ← Brief overlay panel
└── toast.html ← Toast notification
4. Komponen UI & State Management
4.1 Navigasi (HTMX)
Sidebar navigation menggunakan HTMX untuk swap halaman:
<a href="/hosts"
hx-get="/hosts"
hx-target="#main-content"
hx-push-url="true"
class="nav-item {{if eq .View "hosts"}}active{{end}}">
{{svg "server"}}
<span>Hosts</span>
</a>
4.2 State Client (Alpine.js)
State yang butuh client-side reactivity:
<!-- Toggle view mode -->
<div x-data="{ view: 'grid' }">
<button @click="view = 'list'" :class="view === 'list' ? 'bg-primary text-white' : ''">
List
</button>
<div x-show="view === 'grid'" id="host-grid">
{{template "host_list" .}}
</div>
</div>
<!-- Modal -->
<div x-data="{ open: false }">
<button @click="open = true">Add Host</button>
<div x-show="open" class="modal-backdrop">
<div @click.outside="open = false" class="modal-content">
<form hx-post="/hosts" hx-target="#host-list" @submit="open = false">
...
</form>
</div>
</div>
</div>
<!-- Toast -->
<div x-data="{ toast: '', show: false }"
x-init="$watch('show', v => v && setTimeout(() => show = false, 3000))"
x-show="show"
class="toast">
<span x-text="toast"></span>
</div>
4.3 JS Murni (Vanilla)
// clipboard.js — copy to clipboard
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
// trigger toast via Alpine
document.querySelector('[x-data]').__x.$data.toast = 'Copied!';
document.querySelector('[x-data]').__x.$data.show = true;
}
// toggles.js — custom toggle switch
function toggleSwitch(el) {
const checked = el.getAttribute('aria-checked') === 'true';
el.setAttribute('aria-checked', !checked);
fetch(`/settings/toggle?key=${el.dataset.key}&val=${!checked}`, { method: 'POST' });
}
5. Data Models (Go Structs)
Semua data model di internal/model/, mapping dari template React types.ts:
// model/host.go
type Host struct {
ID string `json:"id"`
Name string `json:"name"`
IP string `json:"ip"`
OS string `json:"os"`
Provider string `json:"provider"`
Status string `json:"status"` // "active" | "offline"
LastSeen string `json:"lastSeen"`
Type string `json:"type"` // "api" | "db" | "edge" | "web" | "desktop" | "server"
}
// model/snippet.go
type Snippet struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Code string `json:"code"`
Tags []string `json:"tags"`
Collection string `json:"collection"` // "favorites" | "recent" | "all"
UpdatedAt string `json:"updatedAt"`
Icon string `json:"icon"`
}
Lihat docs/DATA_MODELS.md untuk definisi lengkap.
6. HTMX Patterns yang Digunakan
| Pattern | Contoh | Keterangan |
|---|---|---|
| Search | hx-get="/hosts?q=..." hx-trigger="keyup delay:200ms" hx-target="#host-list" |
Debounced search |
| Filter | hx-get="/hosts?filter=active" hx-target="#host-list" |
Tab filter |
| Modal | Alpine x-show + hx-post |
Form dalam modal |
| Infinite scroll | hx-get="/hosts?page=2" hx-trigger="revealed" hx-target="#host-list" hx-swap="beforeend" |
Load more |
| Delete | hx-delete="/hosts/123" hx-target="#host-card-123" hx-swap="delete" hx-confirm="Yakin?" |
Confirm + delete |
| Toast | Alpine state + hx-trigger="htmx:afterRequest" dari response header HX-Trigger: showToast |
Notifikasi |
| Tab | Alpine x-data + x-show |
Panel tab |
7. Keuntungan HTMX Dibanding React
| Aspek | React (sebelumnya) | HTMX (sekarang) |
|---|---|---|
| Dependency JS | 10+ library (React, Zustand, dll) | 3 library (HTMX, Alpine, xterm) |
| Baris JS | ~2.878 baris TS/JSX | ~410 baris vanilla JS |
| Build step | Vite + TypeScript + npm | CSS build + copy static |
| Bahasa dominan | 60% TS + 40% Go | 90% Go + 10% HTML/JS |
| State management | Zustand store | Server-side (HTMX) + Alpine |
| API layer | REST JSON + manual fetch | HTML partials langsung |
| Electron | Wajib (SPA) | Opsional (bisa browser) |
| Debugging | React DevTools | Browser DevTools (HTML langsung) |
| Time to implement | ~6 minggu | ~4 minggu (estimasi) |
8. Cara Kerja: React → HTMX Mapping per Halaman
Dashboard (Host List)
| React | HTMX |
|---|---|
useState searchQuery |
hx-get="/hosts?q={search}" |
useState statusFilter |
hx-get="/hosts?filter=active" |
useState viewMode |
Alpine x-data="{ view: 'grid' }" |
useState showAddModal |
Alpine x-show="open" |
onChange filter |
HTML <select> + hx-get |
onSubmit add host |
hx-post="/hosts" |
transfers progress |
{{range}} template loop |
Terminal
| React | HTMX |
|---|---|
useState tabs |
Alpine x-data="{ tabs: [], active: '1' }" |
useRef scroll |
xterm.scrollToBottom() (built-in) |
useEffect autoScroll |
terminal.onData(() => scrollToBottom()) |
| Command parser | WebSocket → Go backend → SSH |
| Multi-tab | Alpine + multiple xterm instances |
SFTP
| React | HTMX |
|---|---|
useState local/remote path |
hx-get="/sftp/ls?path=..." hx-target="#pane" |
useState search |
hx-get="/sftp/ls?q=..." hx-trigger="keyup" |
| File click navigation | hx-get="/sftp/ls?path={dir}" hx-push-url |
| Upload/download | hx-post="/sftp/upload" |
| Toast | Alpine x-data + setTimeout |
Snippets
| React | HTMX |
|---|---|
useState search/tag/collection |
hx-get="/snippets?q=&tag=&col=" |
useState copiedId |
JS copyToClipboard() |
useState showAddForm |
Alpine x-show="open" |
| Copy | onclick="copyToClipboard('code')" |
Keychain
| React | HTMX |
|---|---|
useState search |
hx-get="/keys?q=..." |
useState revealPassId |
Alpine x-data="{ reveal: false }" |
useState copiedId |
JS copyToClipboard() |
| Strength badge | Go template: {{if gt (len .Passphrase) 12}}secure{{end}} |
Settings
| React | HTMX |
|---|---|
| Form state | HTML <form> + hx-post="/settings" |
| Toggle switches | JS click handler + fetch POST /settings/toggle |
| Danger zone | onclick="localStorage.clear(); location.reload()" |
Brief Overlay
| React | HTMX |
|---|---|
isOpen + onClose |
Alpine x-show="open" + @click="open = false" |
| Konten statis | Go template render (HTML murni) |
9. File yang Tidak Berubah
Dokumen ini melengkapi (bukan mengganti) dokumen yang masih relevan:
| Dokumen | Status | Alasan |
|---|---|---|
docs/DATA_MODELS.md |
✅ Tetap | Go struct masih sama |
docs/API.md |
✅ Tetap | REST endpoint masih sama |
docs/PERFORMANCE.md |
✅ Tetap | Performance tips masih relevan |
docs/UI_TERMIUS_REFERENCE.md |
✅ Tetap | Visual design masih sama |
docs/ARCHITECTURE.md |
🔄 Diupdate | Ditimpa dengan ringkasan + link ke ARCHITECTURE_HTMX.md |
docs/SPRINT_PLAN.md |
🔄 Diupdate | Sprint tasks diubah ke HTMX |
docs/UI_COMPONENTS.md |
🔄 Diganti | React → HTMX + Alpine |
docs/BUILD_SYSTEM.md |
🔄 Diupdate | Hapus Vite, tambah CSS build |
docs/TIMELINE.md |
🔄 Diupdate | Timeline disesuaikan |
docs/PROGRESS.md |
🔄 Diupdate | Status tracker diperbarui |