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
386 lines
15 KiB
Markdown
386 lines
15 KiB
Markdown
# 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:
|
|
|
|
```html
|
|
<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:
|
|
|
|
```html
|
|
<!-- 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)
|
|
|
|
```javascript
|
|
// 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`:
|
|
|
|
```go
|
|
// 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 |
|