docs: migrate all planning docs from React to GoFiber + HTMX
- 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
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user