Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6b5554028 |
@@ -40,8 +40,3 @@ Thumbs.db
|
|||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
dist/hostkeeper
|
dist/hostkeeper
|
||||||
v1/hostkeeper
|
|
||||||
|
|
||||||
# Node dependencies (for TailwindCSS build)
|
|
||||||
node_modules/
|
|
||||||
package-lock.json
|
|
||||||
|
|||||||
@@ -1,205 +0,0 @@
|
|||||||
# Hostkeeper V2 — AI Session Checkpoint
|
|
||||||
|
|
||||||
> **Purpose**: Memungkinkan AI agent berikutnya melanjutkan development tanpa mengulang atau merusak.
|
|
||||||
> **Last Updated**: 2026-07-07
|
|
||||||
> **Current Phase**: Pre-development (documentation updated, scaffolding not yet started)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Repository Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
hostkeeper/ (root — bersih untuk V2)
|
|
||||||
├── v1/ ← FROZEN — V1 CLI/TUI, jangan disentuh
|
|
||||||
│ ├── cmd/hostkeeper/ V1 cobra CLI commands
|
|
||||||
│ ├── internal/errors/ V1 error handling
|
|
||||||
│ ├── internal/models/ V1 data models
|
|
||||||
│ ├── pkg/ Shared packages (ssh, crypto, storage, config, knownhosts, tui)
|
|
||||||
│ ├── test/ V1 integration & unit tests (105 passing)
|
|
||||||
│ ├── docs/plans/ V1 design & implementation docs
|
|
||||||
│ ├── go.mod module git.tukangketik.id/swanadiva/hostkeeper
|
|
||||||
│ ├── Makefile
|
|
||||||
│ └── PROJECT_STATE.md V1 full project state
|
|
||||||
├── app/ ← (BELUM DIBUAT) V2 backend GoFiber + views + static
|
|
||||||
├── mobile/ ← (BELUM DIBUAT) V2 mobile wrapper
|
|
||||||
├── template/ ← Referensi UI React (Lumina System) — hanya untuk desain
|
|
||||||
│ └── src/components/ 7 React screens
|
|
||||||
├── docs/ ← Dokumentasi perencanaan V2 (HTMX)
|
|
||||||
│ ├── ARCHITECTURE_HTMX.md Arsitektur HTMX (dokumen utama)
|
|
||||||
│ ├── ARCHITECTURE.md Ringkasan + pointer ke HTMX
|
|
||||||
│ ├── API.md REST + WebSocket spec
|
|
||||||
│ ├── DATA_MODELS.md Data models (Host, Snippet, Key, dll)
|
|
||||||
│ ├── UI_COMPONENTS.md HTMX component tree (bukan React)
|
|
||||||
│ ├── SPRINT_PLAN.md Sprint 0-6 untuk HTMX
|
|
||||||
│ ├── BUILD_SYSTEM.md Build pipeline (Go + CSS, tanpa Vite)
|
|
||||||
│ ├── TIMELINE.md Timeline 3.5 minggu
|
|
||||||
│ ├── PERFROMANCE.md Performance & stability
|
|
||||||
│ ├── UI_TERMIUS_REFERENCE.md Termius visual reference
|
|
||||||
│ └── PROGRESS.md Status tracker
|
|
||||||
├── AGENTS.md ← file ini (checkpoint AI)
|
|
||||||
└── README.md
|
|
||||||
|
|
||||||
Git branches:
|
|
||||||
- main → V2 development (current)
|
|
||||||
- v1-snapshot → V1 snapshot before move (safety backup)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. V1 Status — FROZEN 🧊
|
|
||||||
|
|
||||||
| Aspek | Detail |
|
|
||||||
|-------|--------|
|
|
||||||
| Kode | Semua di `v1/` — cmd/, internal/, pkg/, test/ |
|
|
||||||
| Build | `cd v1 && go build ./cmd/hostkeeper` |
|
|
||||||
| Test | `cd v1 && go test ./...` — 105 tests passing |
|
|
||||||
| Branch backup | `v1-snapshot` — snapshot sebelum dipindah ke subdirektori |
|
|
||||||
| Aturan | **JANGAN** mengubah file di `v1/` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. V2 Architecture — HTMX (Keputusan Penting)
|
|
||||||
|
|
||||||
**Hostkeeper V2 menggunakan GoFiber + HTMX, BUKAN React.**
|
|
||||||
|
|
||||||
Alasan:
|
|
||||||
- Developer lebih nyaman dengan Go daripada JavaScript
|
|
||||||
- ~90% kode adalah Go + HTML template
|
|
||||||
- Hanya ~410 baris JS yang diperlukan (xterm.js, clipboard, toggle)
|
|
||||||
- Build lebih sederhana — tanpa Vite, npm, TypeScript, Webpack
|
|
||||||
- Bisa berjalan di browser biasa tanpa Electron
|
|
||||||
|
|
||||||
```
|
|
||||||
┌───────────────────────────────────────────────────┐
|
|
||||||
│ Browser / Electron WebView │
|
|
||||||
│ ┌─────────────────────────────────────────────┐ │
|
|
||||||
│ │ HTMX (14kb) + Alpine.js (15kb) + xterm.js │ │
|
|
||||||
│ │ ● HTMX: search, filter, modal, form, nav │ │
|
|
||||||
│ │ ● Alpine: client state (toast, toggle) │ │
|
|
||||||
│ │ ● xterm.js: terminal (halaman terminal) │ │
|
|
||||||
│ │ ● Vanilla JS: clipboard, toggle, utils │ │
|
|
||||||
│ └────────────────┬────────────────────────────┘ │
|
|
||||||
│ │ hx-get/hx-post/hx-ws │
|
|
||||||
│ ┌────────────────▼────────────────────────────┐ │
|
|
||||||
│ │ GoFiber v2 (localhost:1947) │ │
|
|
||||||
│ │ ┌──────────────┐ ┌────────────────────┐ │ │
|
|
||||||
│ │ │ HTML handler │ │ WebSocket/SSH │ │ │
|
|
||||||
│ │ │ (html/templ.) │ │ (xterm.js I/O) │ │ │
|
|
||||||
│ │ └──────┬───────┘ └────────────────────┘ │ │
|
|
||||||
│ │ ┌────▼────┐ │ │
|
|
||||||
│ │ │ v1/pkg/ │ (via replace directive) │ │
|
|
||||||
│ │ └─────────┘ │ │
|
|
||||||
│ └──────────────────────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
| Library | Ukuran | Fungsi |
|
|
||||||
|---------|--------|--------|
|
|
||||||
| HTMX | 14kb | HTML-over-HTTP interaktivitas |
|
|
||||||
| Alpine.js | 15kb | Client state (modal, toast, tab) |
|
|
||||||
| xterm.js | ~100kb | Terminal emulator (WAJIB) |
|
|
||||||
| TailwindCSS v4 | ~27kb | Utility-first CSS |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. V2 Status — Pre-Development
|
|
||||||
|
|
||||||
### ✅ Sudah Selesai
|
|
||||||
- Semua dokumentasi diupdate ke HTMX (10 file di `docs/`)
|
|
||||||
- Template UI dianalisis (Lumina System — bright-tech glassmorphic, TailwindCSS v4)
|
|
||||||
- Root dibersihkan — V1 di `v1/`, V2 siap dimulai di `app/`
|
|
||||||
- `template/` tetap sebagai referensi desain (warna, layout, spacing)
|
|
||||||
- Bug template sudah diidentifikasi (animasi CSS, w-4.5, error boundary, dll)
|
|
||||||
|
|
||||||
### ❌ Belum Dimulai
|
|
||||||
- `app/backend/` — GoFiber v2 server + handler + views
|
|
||||||
|
|
||||||
### 🐛 Bug Template (dari template/src/ — catatan saat implementasi)
|
|
||||||
1. **P0**: Missing CSS keyframes (fade-in, scale-up, slide-in) → tambah di `custom.css`
|
|
||||||
2. **P1**: `w-4.5 h-4.5` → ganti `w-5 h-5`
|
|
||||||
3. **P2**: Tidak ada Error Boundary → not applicable (HTML template)
|
|
||||||
4. **P2**: `as any` casts → not applicable (Go typed)
|
|
||||||
5. **P3**: Auto-focus loss → handle via JS `focus()` di Alpine
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Struktur Folder V2 (Target)
|
|
||||||
|
|
||||||
```
|
|
||||||
app/backend/
|
|
||||||
├── main.go ← Fiber app entry
|
|
||||||
├── go.mod ← module git.tukangketik.id/swanadiva/hostkeeper/v2
|
|
||||||
├── internal/
|
|
||||||
│ ├── handler/ ← HTTP handlers (render HTML)
|
|
||||||
│ │ ├── dashboard.go
|
|
||||||
│ │ ├── terminal.go
|
|
||||||
│ │ ├── sftp.go
|
|
||||||
│ │ ├── snippets.go
|
|
||||||
│ │ ├── keychain.go
|
|
||||||
│ │ ├── settings.go
|
|
||||||
│ │ ├── brief.go
|
|
||||||
│ │ └── health.go
|
|
||||||
│ ├── model/ ← Go structs (Host, Snippet, Key, dll)
|
|
||||||
│ ├── template/ ← Template helper funcs
|
|
||||||
│ └── middleware/ ← View data injection
|
|
||||||
├── static/
|
|
||||||
│ ├── css/ ← output.css + custom.css
|
|
||||||
│ ├── js/ ← htmx.min.js, alpine.min.js, utils.js, terminal.js
|
|
||||||
│ └── xterm/ ← xterm.js lib
|
|
||||||
└── views/
|
|
||||||
├── layout.html
|
|
||||||
├── index.html
|
|
||||||
└── partials/ ← ~17 partial HTML files
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Yang Harus Dilakukan Selanjutnya (First Actions)
|
|
||||||
|
|
||||||
Urutan eksekusi saat development dimulai:
|
|
||||||
|
|
||||||
```
|
|
||||||
Sprint 0 — Setup:
|
|
||||||
Step 1: mkdir -p app/backend
|
|
||||||
Step 2: cd app/backend && go mod init git.tukangketik.id/swanadiva/hostkeeper/v2
|
|
||||||
Step 3: go get github.com/gofiber/fiber/v2
|
|
||||||
Step 4: go get github.com/gofiber/template/html/v2
|
|
||||||
Step 5: Buat main.go (Fiber app skeleton + static + template engine)
|
|
||||||
Step 6: Download htmx.min.js, alpine.min.js ke static/js/
|
|
||||||
Step 7: Setup TailwindCSS v4: input.css → npx @tailwindcss/cli
|
|
||||||
Step 8: Buat custom.css (dot-grid, glassmorphism, keyframes)
|
|
||||||
Step 9: Buat views/layout.html + views/index.html + nav.html
|
|
||||||
Step 10: Buat handler/health.go + handler/dashboard.go
|
|
||||||
Step 11: Verify: buka http://localhost:1947 — layout terlihat
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Path Penting & Referensi
|
|
||||||
|
|
||||||
| Untuk | Lihat di |
|
|
||||||
|-------|----------|
|
|
||||||
| Arsitektur HTMX lengkap | `docs/ARCHITECTURE_HTMX.md` |
|
|
||||||
| API spec | `docs/API.md` |
|
|
||||||
| Data models (Go struct) | `docs/DATA_MODELS.md` |
|
|
||||||
| HTMX component tree | `docs/UI_COMPONENTS.md` |
|
|
||||||
| Sprint plan detail | `docs/SPRINT_PLAN.md` |
|
|
||||||
| Timeline 3.5 minggu | `docs/TIMELINE.md` |
|
|
||||||
| Build system | `docs/BUILD_SYSTEM.md` |
|
|
||||||
| Status tracker | `docs/PROGRESS.md` |
|
|
||||||
| Template desain (React) | `template/AGENTS.md` (Lumina System) |
|
|
||||||
| Template komponen (referensi) | `template/src/components/` |
|
|
||||||
| V1 shared packages | `v1/pkg/ssh/`, `v1/pkg/crypto/`, `v1/pkg/storage/`, `v1/pkg/config/`, `v1/pkg/knownhosts/` |
|
|
||||||
| V1 build & test | `cd v1 && go build ./cmd/hostkeeper && go test ./...` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Aturan Penting
|
|
||||||
|
|
||||||
- **JANGAN** mengubah file di `v1/` — V1 FROZEN
|
|
||||||
- **JANGAN** mengubah `template/` — itu referensi desain, bukan source aktif
|
|
||||||
- Semua kode V2 baru di `app/`
|
|
||||||
- Saat reuse `v1/pkg/*`, pakai `go.mod replace` — jangan copy
|
|
||||||
- Template React hanya untuk referensi visual (warna, layout, spacing)
|
|
||||||
- Gunakan TailwindCSS v4 + custom CSS (Lumina System)
|
|
||||||
- Pakai bahasa Indonesia untuk komunikasi dengan user (swanadiva)
|
|
||||||
+9
-9
@@ -3,15 +3,15 @@
|
|||||||
## [Unreleased] — V2 Planning Complete (2026-06-29)
|
## [Unreleased] — V2 Planning Complete (2026-06-29)
|
||||||
|
|
||||||
### Added (V2 Planning Documents)
|
### Added (V2 Planning Documents)
|
||||||
- `docs/PROGRESS.md` — Status tracker for V2 development
|
- `docs/v2/PROGRESS.md` — Status tracker for V2 development
|
||||||
- `docs/ARCHITECTURE.md` — System architecture (GoFiber + React + Electron)
|
- `docs/v2/ARCHITECTURE.md` — System architecture (GoFiber + React + Electron)
|
||||||
- `docs/API.md` — REST API + WebSocket specification
|
- `docs/v2/API.md` — REST API + WebSocket specification
|
||||||
- `docs/DATA_MODELS.md` — Extended data models (Groups, PortForward, Workspace)
|
- `docs/v2/DATA_MODELS.md` — Extended data models (Groups, PortForward, Workspace)
|
||||||
- `docs/UI_COMPONENTS.md` — React component tree + state management
|
- `docs/v2/UI_COMPONENTS.md` — React component tree + state management
|
||||||
- `docs/UI_TERMIUS_REFERENCE.md` — Termius visual reference
|
- `docs/v2/UI_TERMIUS_REFERENCE.md` — Termius visual reference
|
||||||
- `docs/SPRINT_PLAN.md` — Sprint-by-sprint task breakdown
|
- `docs/v2/SPRINT_PLAN.md` — Sprint-by-sprint task breakdown
|
||||||
- `docs/BUILD_SYSTEM.md` — Cross-platform build pipeline
|
- `docs/v2/BUILD_SYSTEM.md` — Cross-platform build pipeline
|
||||||
- `docs/PERFORMANCE.md` — Performance & stability guide
|
- `docs/v2/PERFORMANCE.md` — Performance & stability guide
|
||||||
|
|
||||||
### Architecture Decision
|
### Architecture Decision
|
||||||
- Backend: GoFiber v2 (fasthttp-based) replacing chi/net/http
|
- Backend: GoFiber v2 (fasthttp-based) replacing chi/net/http
|
||||||
|
|||||||
@@ -2,39 +2,136 @@
|
|||||||
|
|
||||||
**Cross-platform SSH/SFTP management tool with secure credential storage**
|
**Cross-platform SSH/SFTP management tool with secure credential storage**
|
||||||
|
|
||||||
## V1 (Legacy) — CLI/TUI
|
## Features
|
||||||
|
|
||||||
V1 adalah versi CLI/TUI yang sudah selesai dan di-freeze. Semua kode ada di folder [`v1/`](v1/).
|
- **Secure Credential Management** — Store SSH credentials with proper file permissions (0600)
|
||||||
|
- **SSH Connection** — Connect to hosts via native SSH or Go SSH client
|
||||||
|
- **Host Management** — Add, list, connect, edit, delete hosts
|
||||||
|
- **Export/Import** — Backup and transfer credentials across devices
|
||||||
|
- **TUI Interface** — Interactive terminal user interface for host management
|
||||||
|
- **Cross-Platform** — Works on Linux, macOS, Windows, and Termux (Android)
|
||||||
|
- **Fast CLI** — Quick commands for power users
|
||||||
|
- **Tag-based Organization** — Categorize hosts with custom tags and groups
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd v1
|
# Build from source
|
||||||
|
git clone https://git.tukangketik.id/swanadiva/HostKeeper.git
|
||||||
|
cd hostkeeper
|
||||||
make build
|
make build
|
||||||
./hostkeeper tui
|
|
||||||
|
# Or install directly
|
||||||
|
go install git.tukangketik.id/swanadiva/hostkeeper/cmd/hostkeeper@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Lihat [`v1/README.md`](v1/README.md) untuk dokumentasi lengkap.
|
### Usage
|
||||||
|
|
||||||
## V2 (In Development) — GoFiber + React + Electron
|
```bash
|
||||||
|
# Add a host (interactive)
|
||||||
|
hostkeeper add
|
||||||
|
|
||||||
V2 adalah rewrite dengan arsitektur baru:
|
# Or with flags
|
||||||
- **Backend**: GoFiber v2 (fasthttp) — lokal HTTP server di `app/backend/`
|
hostkeeper add myserver --host 192.168.1.10 --user admin --password mypass
|
||||||
- **Frontend**: React + TypeScript + TailwindCSS v4 di `app/frontend/`
|
|
||||||
- **Desktop**: Electron wrapper di `app/electron/`
|
|
||||||
- **Mobile**: WebView shell di `mobile/`
|
|
||||||
|
|
||||||
Lihat [docs/](docs/) untuk dokumentasi perencanaan.
|
# List all hosts
|
||||||
|
hostkeeper list
|
||||||
|
|
||||||
## Struktur Repository
|
# Connect to host
|
||||||
|
hostkeeper connect myserver
|
||||||
|
|
||||||
|
# Edit host
|
||||||
|
hostkeeper edit myserver
|
||||||
|
|
||||||
|
# Delete host
|
||||||
|
hostkeeper delete myserver
|
||||||
|
|
||||||
|
# Launch TUI
|
||||||
|
hostkeeper tui
|
||||||
```
|
```
|
||||||
hostkeeper/
|
|
||||||
├── v1/ ← V1 CLI/TUI (frozen)
|
## Core Commands
|
||||||
├── app/ ← V2 backend + frontend + electron
|
|
||||||
├── mobile/ ← V2 mobile wrapper
|
| Command | Description |
|
||||||
├── template/ ← Referensi UI React (Lumina System)
|
|---------|-------------|
|
||||||
└── docs/ ← Dokumentasi perencanaan V2
|
| `hostkeeper add [name]` | Add a new SSH host |
|
||||||
|
| `hostkeeper list` | List all saved hosts |
|
||||||
|
| `hostkeeper connect <name>` | Connect to a host |
|
||||||
|
| `hostkeeper edit <name>` | Edit a host configuration |
|
||||||
|
| `hostkeeper delete <name>` | Delete a host |
|
||||||
|
| `hostkeeper export <file>` | Export data to JSON file |
|
||||||
|
| `hostkeeper import <file>` | Import data from JSON file |
|
||||||
|
| `hostkeeper tui` | Launch TUI interface |
|
||||||
|
| `hostkeeper completion [shell]` | Generate shell completion |
|
||||||
|
| `hostkeeper version` | Print version info |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
See [Installation Guide](docs/INSTALLATION.md) for detailed instructions for all platforms.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
See [Usage Guide](docs/USAGE.md) for detailed command examples.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
See [Architecture Overview](docs/ARCHITECTURE.md) for technical details.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- All credential files use `0600` permissions (owner read/write only)
|
||||||
|
- Passwords and keys are never logged or displayed in error messages
|
||||||
|
- Encrypted storage planned for Phase 2
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Go 1.21+
|
||||||
|
- Make (optional, for build automation)
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.tukangketik.id/swanadiva/HostKeeper.git
|
||||||
|
cd hostkeeper
|
||||||
|
make deps
|
||||||
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
make test-coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Current platform
|
||||||
|
make build
|
||||||
|
|
||||||
|
# All platforms
|
||||||
|
make release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Language**: Go 1.21+
|
||||||
|
- **CLI Framework**: [Cobra](https://github.com/spf13/cobra)
|
||||||
|
- **TUI Framework**: [Bubble Tea](https://github.com/charmbracelet/bubbletea)
|
||||||
|
- **SSH Library**: [golang.org/x/crypto/ssh](https://pkg.go.dev/golang.org/x/crypto/ssh)
|
||||||
|
|
||||||
|
## Project Status
|
||||||
|
|
||||||
|
Project is in active development. See [PROJECT_STATE.md](PROJECT_STATE.md) for current progress.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
module git.tukangketik.id/swanadiva/hostkeeper/v2
|
|
||||||
|
|
||||||
go 1.26.4
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/gofiber/fiber/v2 v2.52.14
|
|
||||||
github.com/gofiber/template/html/v2 v2.1.3
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
|
||||||
github.com/gofiber/template v1.8.3 // indirect
|
|
||||||
github.com/gofiber/utils v1.1.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/gofiber/fiber/v2 v2.52.14 h1:Of3L+9qVFaQNwPlcmEdl5IIodHz8BSE0j37R7rWu4pE=
|
|
||||||
github.com/gofiber/fiber/v2 v2.52.14/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
|
||||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
|
||||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
|
||||||
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
|
|
||||||
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
|
|
||||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
|
||||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
|
||||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
|
||||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
var store = model.NewHostStore()
|
|
||||||
|
|
||||||
func Dashboard(c *fiber.Ctx) error {
|
|
||||||
hosts := store.All()
|
|
||||||
return c.Render("index", fiber.Map{
|
|
||||||
"View": "hosts",
|
|
||||||
"Title": "Hosts",
|
|
||||||
"Hosts": hosts,
|
|
||||||
"Active": countByStatus(hosts, "active"),
|
|
||||||
"Offline": countByStatus(hosts, "offline"),
|
|
||||||
"NavItems": []fiber.Map{
|
|
||||||
{"ID": "hosts", "Label": "Hosts", "Icon": "server"},
|
|
||||||
{"ID": "terminal", "Label": "Terminal", "Icon": "terminal"},
|
|
||||||
{"ID": "sftp", "Label": "SFTP", "Icon": "folder-sync"},
|
|
||||||
{"ID": "snippets", "Label": "Snippets", "Icon": "code2"},
|
|
||||||
{"ID": "keychain", "Label": "Keychain", "Icon": "key-round"},
|
|
||||||
{"ID": "settings", "Label": "Settings", "Icon": "settings2"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func DashboardList(c *fiber.Ctx) error {
|
|
||||||
q := c.Query("q")
|
|
||||||
filter := c.Query("filter", "all")
|
|
||||||
hosts := store.Search(q, filter)
|
|
||||||
return c.Render("host_list", fiber.Map{
|
|
||||||
"Hosts": hosts,
|
|
||||||
"Active": countByStatus(hosts, "active"),
|
|
||||||
"Offline": countByStatus(hosts, "offline"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateHost(c *fiber.Ctx) error {
|
|
||||||
name := c.FormValue("name")
|
|
||||||
ip := c.FormValue("ip")
|
|
||||||
os := c.FormValue("os")
|
|
||||||
provider := c.FormValue("provider")
|
|
||||||
hostType := c.FormValue("type")
|
|
||||||
|
|
||||||
if name == "" || ip == "" {
|
|
||||||
return c.Status(400).SendString("name and ip required")
|
|
||||||
}
|
|
||||||
|
|
||||||
store.Add(name, ip, os, provider, hostType)
|
|
||||||
hosts := store.All()
|
|
||||||
return c.Render("host_list", fiber.Map{
|
|
||||||
"Hosts": hosts,
|
|
||||||
"Active": countByStatus(hosts, "active"),
|
|
||||||
"Offline": countByStatus(hosts, "offline"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteHost(c *fiber.Ctx) error {
|
|
||||||
id := c.Params("id")
|
|
||||||
store.Delete(id)
|
|
||||||
hosts := store.All()
|
|
||||||
return c.Render("host_list", fiber.Map{
|
|
||||||
"Hosts": hosts,
|
|
||||||
"Active": countByStatus(hosts, "active"),
|
|
||||||
"Offline": countByStatus(hosts, "offline"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func countByStatus(hosts []model.Host, status string) int {
|
|
||||||
n := 0
|
|
||||||
for _, h := range hosts {
|
|
||||||
if h.Status == status {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v2"
|
|
||||||
|
|
||||||
func Health(c *fiber.Ctx) error {
|
|
||||||
return c.JSON(fiber.Map{
|
|
||||||
"status": "ok",
|
|
||||||
"app": "hostkeeper-v2",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v2"
|
|
||||||
|
|
||||||
func ViewData() fiber.Handler {
|
|
||||||
return func(c *fiber.Ctx) error {
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type HostStore struct {
|
|
||||||
Hosts []Host
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHostStore() *HostStore {
|
|
||||||
return &HostStore{
|
|
||||||
Hosts: defaultHosts(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HostStore) All() []Host {
|
|
||||||
return s.Hosts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HostStore) Search(q, filter string) []Host {
|
|
||||||
var result []Host
|
|
||||||
for _, h := range s.Hosts {
|
|
||||||
if filter != "" && filter != "all" && h.Status != filter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if q == "" || strings.Contains(strings.ToLower(h.Name), strings.ToLower(q)) ||
|
|
||||||
strings.Contains(strings.ToLower(h.IP), strings.ToLower(q)) ||
|
|
||||||
strings.Contains(strings.ToLower(h.OS), strings.ToLower(q)) {
|
|
||||||
result = append(result, h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if result == nil {
|
|
||||||
return []Host{}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HostStore) Delete(id string) {
|
|
||||||
for i, h := range s.Hosts {
|
|
||||||
if h.ID == id {
|
|
||||||
s.Hosts = append(s.Hosts[:i], s.Hosts[i+1:]...)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *HostStore) Add(name, ip, os, provider, hostType string) Host {
|
|
||||||
h := Host{
|
|
||||||
ID: randString(8),
|
|
||||||
Name: name,
|
|
||||||
IP: ip,
|
|
||||||
OS: os,
|
|
||||||
Provider: provider,
|
|
||||||
Status: "active",
|
|
||||||
LastSeen: time.Now().Format("Jan 2, 2006"),
|
|
||||||
Type: hostType,
|
|
||||||
}
|
|
||||||
s.Hosts = append(s.Hosts, h)
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func randString(n int) string {
|
|
||||||
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
||||||
b := make([]byte, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = letters[rand.Intn(len(letters))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultHosts() []Host {
|
|
||||||
return []Host{
|
|
||||||
{ID: "h1", Name: "api-prod-01", IP: "10.0.1.15", OS: "Ubuntu 22.04", Provider: "AWS US-East", Status: "active", LastSeen: "Online", Type: "api"},
|
|
||||||
{ID: "h2", Name: "db-primary-01", IP: "10.0.2.5", OS: "Debian 12", Provider: "AWS US-East", Status: "active", LastSeen: "Online", Type: "db"},
|
|
||||||
{ID: "h3", Name: "web-edge-02", IP: "192.168.1.20", OS: "Alpine 3.19", Provider: "DigitalOcean", Status: "offline", LastSeen: "2 hours ago", Type: "edge"},
|
|
||||||
{ID: "h4", Name: "staging-api-01", IP: "10.0.3.10", OS: "Ubuntu 22.04", Provider: "AWS EU-West", Status: "active", LastSeen: "5 min ago", Type: "api"},
|
|
||||||
{ID: "h5", Name: "dev-db-02", IP: "192.168.1.45", OS: "Fedora 39", Provider: "Hetzner", Status: "offline", LastSeen: "1 day ago", Type: "db"},
|
|
||||||
{ID: "h6", Name: "monitor-01", IP: "10.0.0.5", OS: "Ubuntu 24.04", Provider: "AWS US-East", Status: "active", LastSeen: "Just now", Type: "server"},
|
|
||||||
{ID: "h7", Name: "worker-pool-01", IP: "10.0.4.50", OS: "Debian 12", Provider: "GCP US-Central", Status: "active", LastSeen: "1 min ago", Type: "server"},
|
|
||||||
{ID: "h8", Name: "bastion-host", IP: "54.85.12.7", OS: "Ubuntu 22.04", Provider: "AWS US-East", Status: "offline", LastSeen: "3 days ago", Type: "server"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/compress"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
|
||||||
"github.com/gofiber/template/html/v2"
|
|
||||||
|
|
||||||
"git.tukangketik.id/swanadiva/hostkeeper/v2/internal/handler"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
engine := html.New("./views", ".html")
|
|
||||||
engine.Reload(true)
|
|
||||||
engine.AddFunc("svg", func(name string) string {
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{
|
|
||||||
Views: engine,
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Use(logger.New())
|
|
||||||
app.Use(recover.New())
|
|
||||||
app.Use(compress.New())
|
|
||||||
app.Static("/static", "./static")
|
|
||||||
|
|
||||||
app.Get("/", handler.Dashboard)
|
|
||||||
app.Get("/hosts", handler.DashboardList)
|
|
||||||
app.Post("/hosts", handler.CreateHost)
|
|
||||||
app.Delete("/hosts/:id", handler.DeleteHost)
|
|
||||||
app.Get("/api/health", handler.Health)
|
|
||||||
|
|
||||||
log.Fatal(app.Listen(":1947"))
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"type": "commonjs",
|
|
||||||
"devDependencies": {
|
|
||||||
"@tailwindcss/cli": "^4.3.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
/* Lumina System — Custom CSS */
|
|
||||||
|
|
||||||
/* Alpine cloak — hide until initialized */
|
|
||||||
[x-cloak] { display: none !important; }
|
|
||||||
|
|
||||||
/* Dot grid background */
|
|
||||||
.dot-grid {
|
|
||||||
background-image: radial-gradient(#e2e8f0 1.5px, transparent 1.5px);
|
|
||||||
background-size: 16px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Glass panels */
|
|
||||||
.glass-panel {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-sidebar {
|
|
||||||
background: rgba(255, 255, 255, 0.75);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
-webkit-backdrop-filter: blur(16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes fade-in {
|
|
||||||
from { opacity: 0; transform: translateY(8px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scale-up {
|
|
||||||
from { opacity: 0; transform: scale(0.95); }
|
|
||||||
to { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-in {
|
|
||||||
from { opacity: 0; transform: translateX(20px); }
|
|
||||||
to { opacity: 1; transform: translateX(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cursor-blink {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-fade-in {
|
|
||||||
animation: fade-in 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-scale-up {
|
|
||||||
animation: scale-up 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-slide-in {
|
|
||||||
animation: slide-in 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-cursor {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 18px;
|
|
||||||
background: currentColor;
|
|
||||||
animation: cursor-blink 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #c2c6d8;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #a0a4b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-scrollbar {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-scrollbar::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal backdrop */
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.3);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 24px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
|
||||||
animation: scale-up 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast */
|
|
||||||
.toast {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 24px;
|
|
||||||
right: 24px;
|
|
||||||
background: #191c1e;
|
|
||||||
color: white;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
|
||||||
z-index: 100;
|
|
||||||
animation: slide-in 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle switch */
|
|
||||||
.toggle-switch {
|
|
||||||
position: relative;
|
|
||||||
width: 44px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #c2c6d8;
|
|
||||||
transition: background 0.2s;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-switch.active {
|
|
||||||
background: #0050cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-knob {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: white;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-switch.active .toggle-knob {
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* File table */
|
|
||||||
.file-table-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 80px 120px;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-table-row:hover {
|
|
||||||
background: #f1f4f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Strength badges */
|
|
||||||
.strength-badge-secure {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
background: #006e2f/15;
|
|
||||||
color: #006e2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strength-badge-moderate {
|
|
||||||
background: #7e23cc/15;
|
|
||||||
color: #7e23cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strength-badge-weak {
|
|
||||||
background: #b3261e/15;
|
|
||||||
color: #b3261e;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--color-primary: #0050cb;
|
|
||||||
--color-on-primary: #ffffff;
|
|
||||||
--color-primary-container: #d6e3ff;
|
|
||||||
--color-on-primary-container: #001638;
|
|
||||||
--color-secondary: #006e2f;
|
|
||||||
--color-on-secondary: #ffffff;
|
|
||||||
--color-secondary-container: #6bff8f;
|
|
||||||
--color-on-secondary-container: #00210b;
|
|
||||||
--color-tertiary: #7e23cc;
|
|
||||||
--color-on-tertiary: #ffffff;
|
|
||||||
--color-tertiary-container: #f0daff;
|
|
||||||
--color-on-tertiary-container: #290055;
|
|
||||||
--color-error: #b3261e;
|
|
||||||
--color-on-error: #ffffff;
|
|
||||||
--color-error-container: #f9dedc;
|
|
||||||
--color-on-error-container: #410e0b;
|
|
||||||
--color-surface: #f7f9fb;
|
|
||||||
--color-on-surface: #191c1e;
|
|
||||||
--color-surface-container-low: #f0f2f5;
|
|
||||||
--color-surface-container: #eaecf0;
|
|
||||||
--color-surface-container-high: #e2e5e9;
|
|
||||||
--color-on-surface-variant: #424656;
|
|
||||||
--color-outline: #727686;
|
|
||||||
--color-outline-variant: #c2c6d8;
|
|
||||||
--color-inverse-surface: #2d3133;
|
|
||||||
--color-inverse-on-surface: #f0f1f3;
|
|
||||||
|
|
||||||
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
|
||||||
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
||||||
}
|
|
||||||
@@ -1,848 +0,0 @@
|
|||||||
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
|
||||||
@layer properties;
|
|
||||||
@layer theme, base, components, utilities;
|
|
||||||
@layer theme {
|
|
||||||
:root, :host {
|
|
||||||
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
|
||||||
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
||||||
--color-white: #fff;
|
|
||||||
--spacing: 0.25rem;
|
|
||||||
--container-md: 28rem;
|
|
||||||
--container-7xl: 80rem;
|
|
||||||
--text-xs: 0.75rem;
|
|
||||||
--text-xs--line-height: calc(1 / 0.75);
|
|
||||||
--text-sm: 0.875rem;
|
|
||||||
--text-sm--line-height: calc(1.25 / 0.875);
|
|
||||||
--text-lg: 1.125rem;
|
|
||||||
--text-lg--line-height: calc(1.75 / 1.125);
|
|
||||||
--text-2xl: 1.5rem;
|
|
||||||
--text-2xl--line-height: calc(2 / 1.5);
|
|
||||||
--font-weight-medium: 500;
|
|
||||||
--font-weight-semibold: 600;
|
|
||||||
--font-weight-bold: 700;
|
|
||||||
--tracking-wide: 0.025em;
|
|
||||||
--radius-lg: 0.5rem;
|
|
||||||
--radius-xl: 0.75rem;
|
|
||||||
--blur-md: 12px;
|
|
||||||
--default-transition-duration: 150ms;
|
|
||||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--default-font-family: var(--font-sans);
|
|
||||||
--default-mono-font-family: var(--font-mono);
|
|
||||||
--color-primary: #0050cb;
|
|
||||||
--color-secondary: #006e2f;
|
|
||||||
--color-tertiary: #7e23cc;
|
|
||||||
--color-surface: #f7f9fb;
|
|
||||||
--color-on-surface: #191c1e;
|
|
||||||
--color-surface-container-low: #f0f2f5;
|
|
||||||
--color-on-surface-variant: #424656;
|
|
||||||
--color-outline-variant: #c2c6d8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@layer base {
|
|
||||||
*, ::after, ::before, ::backdrop, ::file-selector-button {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0 solid;
|
|
||||||
}
|
|
||||||
html, :host {
|
|
||||||
line-height: 1.5;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
tab-size: 4;
|
|
||||||
font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
|
||||||
font-feature-settings: var(--default-font-feature-settings, normal);
|
|
||||||
font-variation-settings: var(--default-font-variation-settings, normal);
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
hr {
|
|
||||||
height: 0;
|
|
||||||
color: inherit;
|
|
||||||
border-top-width: 1px;
|
|
||||||
}
|
|
||||||
abbr:where([title]) {
|
|
||||||
-webkit-text-decoration: underline dotted;
|
|
||||||
text-decoration: underline dotted;
|
|
||||||
}
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
|
||||||
font-size: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
-webkit-text-decoration: inherit;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
b, strong {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
code, kbd, samp, pre {
|
|
||||||
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
|
|
||||||
font-feature-settings: var(--default-mono-font-feature-settings, normal);
|
|
||||||
font-variation-settings: var(--default-mono-font-variation-settings, normal);
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
small {
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
sub, sup {
|
|
||||||
font-size: 75%;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
sub {
|
|
||||||
bottom: -0.25em;
|
|
||||||
}
|
|
||||||
sup {
|
|
||||||
top: -0.5em;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
text-indent: 0;
|
|
||||||
border-color: inherit;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
:-moz-focusring {
|
|
||||||
outline: auto;
|
|
||||||
}
|
|
||||||
progress {
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
summary {
|
|
||||||
display: list-item;
|
|
||||||
}
|
|
||||||
ol, ul, menu {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
img, svg, video, canvas, audio, iframe, embed, object {
|
|
||||||
display: block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
img, video {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
button, input, select, optgroup, textarea, ::file-selector-button {
|
|
||||||
font: inherit;
|
|
||||||
font-feature-settings: inherit;
|
|
||||||
font-variation-settings: inherit;
|
|
||||||
letter-spacing: inherit;
|
|
||||||
color: inherit;
|
|
||||||
border-radius: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
:where(select:is([multiple], [size])) optgroup {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
:where(select:is([multiple], [size])) optgroup option {
|
|
||||||
padding-inline-start: 20px;
|
|
||||||
}
|
|
||||||
::file-selector-button {
|
|
||||||
margin-inline-end: 4px;
|
|
||||||
}
|
|
||||||
::placeholder {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
|
||||||
::placeholder {
|
|
||||||
color: currentcolor;
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
color: color-mix(in oklab, currentcolor 50%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
::-webkit-search-decoration {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
::-webkit-date-and-time-value {
|
|
||||||
min-height: 1lh;
|
|
||||||
text-align: inherit;
|
|
||||||
}
|
|
||||||
::-webkit-datetime-edit {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
::-webkit-datetime-edit-fields-wrapper {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
|
|
||||||
padding-block: 0;
|
|
||||||
}
|
|
||||||
::-webkit-calendar-picker-indicator {
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
:-moz-ui-invalid {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
|
|
||||||
appearance: button;
|
|
||||||
}
|
|
||||||
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
[hidden]:where(:not([hidden="until-found"])) {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@layer utilities {
|
|
||||||
.collapse {
|
|
||||||
visibility: collapse;
|
|
||||||
}
|
|
||||||
.visible {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
.relative {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.mx-auto {
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
.mb-1 {
|
|
||||||
margin-bottom: var(--spacing);
|
|
||||||
}
|
|
||||||
.mb-2 {
|
|
||||||
margin-bottom: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.mb-6 {
|
|
||||||
margin-bottom: calc(var(--spacing) * 6);
|
|
||||||
}
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
.inline {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.h-4 {
|
|
||||||
height: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.h-5 {
|
|
||||||
height: calc(var(--spacing) * 5);
|
|
||||||
}
|
|
||||||
.h-8 {
|
|
||||||
height: calc(var(--spacing) * 8);
|
|
||||||
}
|
|
||||||
.h-14 {
|
|
||||||
height: calc(var(--spacing) * 14);
|
|
||||||
}
|
|
||||||
.min-h-screen {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.w-4 {
|
|
||||||
width: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.w-5 {
|
|
||||||
width: calc(var(--spacing) * 5);
|
|
||||||
}
|
|
||||||
.w-8 {
|
|
||||||
width: calc(var(--spacing) * 8);
|
|
||||||
}
|
|
||||||
.w-\[260px\] {
|
|
||||||
width: 260px;
|
|
||||||
}
|
|
||||||
.w-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.max-w-7xl {
|
|
||||||
max-width: var(--container-7xl);
|
|
||||||
}
|
|
||||||
.max-w-md {
|
|
||||||
max-width: var(--container-md);
|
|
||||||
}
|
|
||||||
.min-w-0 {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.flex-1 {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.shrink-0 {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.transform {
|
|
||||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
|
||||||
}
|
|
||||||
.cursor-pointer {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.resize {
|
|
||||||
resize: both;
|
|
||||||
}
|
|
||||||
.grid-cols-1 {
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
.grid-cols-3 {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
.flex-col {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.items-center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.justify-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.gap-2 {
|
|
||||||
gap: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.gap-3 {
|
|
||||||
gap: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
.gap-4 {
|
|
||||||
gap: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.space-y-0\.5 {
|
|
||||||
:where(& > :not(:last-child)) {
|
|
||||||
--tw-space-y-reverse: 0;
|
|
||||||
margin-block-start: calc(calc(var(--spacing) * 0.5) * var(--tw-space-y-reverse));
|
|
||||||
margin-block-end: calc(calc(var(--spacing) * 0.5) * calc(1 - var(--tw-space-y-reverse)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.truncate {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.overflow-auto {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
.overflow-hidden {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.rounded {
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
.rounded-full {
|
|
||||||
border-radius: calc(infinity * 1px);
|
|
||||||
}
|
|
||||||
.rounded-lg {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
}
|
|
||||||
.rounded-xl {
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
}
|
|
||||||
.border {
|
|
||||||
border-style: var(--tw-border-style);
|
|
||||||
border-width: 1px;
|
|
||||||
}
|
|
||||||
.border-2 {
|
|
||||||
border-style: var(--tw-border-style);
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
.border-t {
|
|
||||||
border-top-style: var(--tw-border-style);
|
|
||||||
border-top-width: 1px;
|
|
||||||
}
|
|
||||||
.border-r {
|
|
||||||
border-right-style: var(--tw-border-style);
|
|
||||||
border-right-width: 1px;
|
|
||||||
}
|
|
||||||
.border-b {
|
|
||||||
border-bottom-style: var(--tw-border-style);
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
}
|
|
||||||
.border-dashed {
|
|
||||||
--tw-border-style: dashed;
|
|
||||||
border-style: dashed;
|
|
||||||
}
|
|
||||||
.border-outline-variant\/20 {
|
|
||||||
border-color: color-mix(in srgb, #c2c6d8 20%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
border-color: color-mix(in oklab, var(--color-outline-variant) 20%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.border-outline-variant\/30 {
|
|
||||||
border-color: color-mix(in srgb, #c2c6d8 30%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
border-color: color-mix(in oklab, var(--color-outline-variant) 30%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.border-outline-variant\/50 {
|
|
||||||
border-color: color-mix(in srgb, #c2c6d8 50%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
border-color: color-mix(in oklab, var(--color-outline-variant) 50%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bg-primary {
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
.bg-primary\/10 {
|
|
||||||
background-color: color-mix(in srgb, #0050cb 10%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
background-color: color-mix(in oklab, var(--color-primary) 10%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bg-primary\/20 {
|
|
||||||
background-color: color-mix(in srgb, #0050cb 20%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
background-color: color-mix(in oklab, var(--color-primary) 20%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bg-secondary\/20 {
|
|
||||||
background-color: color-mix(in srgb, #006e2f 20%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
background-color: color-mix(in oklab, var(--color-secondary) 20%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bg-surface {
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
}
|
|
||||||
.bg-tertiary\/20 {
|
|
||||||
background-color: color-mix(in srgb, #7e23cc 20%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
background-color: color-mix(in oklab, var(--color-tertiary) 20%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.bg-white {
|
|
||||||
background-color: var(--color-white);
|
|
||||||
}
|
|
||||||
.bg-white\/70 {
|
|
||||||
background-color: color-mix(in srgb, #fff 70%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
background-color: color-mix(in oklab, var(--color-white) 70%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.p-0\.5 {
|
|
||||||
padding: calc(var(--spacing) * 0.5);
|
|
||||||
}
|
|
||||||
.p-1\.5 {
|
|
||||||
padding: calc(var(--spacing) * 1.5);
|
|
||||||
}
|
|
||||||
.p-4 {
|
|
||||||
padding: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.p-6 {
|
|
||||||
padding: calc(var(--spacing) * 6);
|
|
||||||
}
|
|
||||||
.p-8 {
|
|
||||||
padding: calc(var(--spacing) * 8);
|
|
||||||
}
|
|
||||||
.px-3 {
|
|
||||||
padding-inline: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
.px-6 {
|
|
||||||
padding-inline: calc(var(--spacing) * 6);
|
|
||||||
}
|
|
||||||
.py-2 {
|
|
||||||
padding-block: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.py-4 {
|
|
||||||
padding-block: calc(var(--spacing) * 4);
|
|
||||||
}
|
|
||||||
.pr-3 {
|
|
||||||
padding-right: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
.pl-9 {
|
|
||||||
padding-left: calc(var(--spacing) * 9);
|
|
||||||
}
|
|
||||||
.text-2xl {
|
|
||||||
font-size: var(--text-2xl);
|
|
||||||
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
|
||||||
}
|
|
||||||
.text-lg {
|
|
||||||
font-size: var(--text-lg);
|
|
||||||
line-height: var(--tw-leading, var(--text-lg--line-height));
|
|
||||||
}
|
|
||||||
.text-sm {
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
|
||||||
}
|
|
||||||
.text-xs {
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
line-height: var(--tw-leading, var(--text-xs--line-height));
|
|
||||||
}
|
|
||||||
.font-bold {
|
|
||||||
--tw-font-weight: var(--font-weight-bold);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
}
|
|
||||||
.font-medium {
|
|
||||||
--tw-font-weight: var(--font-weight-medium);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
|
||||||
.font-semibold {
|
|
||||||
--tw-font-weight: var(--font-weight-semibold);
|
|
||||||
font-weight: var(--font-weight-semibold);
|
|
||||||
}
|
|
||||||
.tracking-wide {
|
|
||||||
--tw-tracking: var(--tracking-wide);
|
|
||||||
letter-spacing: var(--tracking-wide);
|
|
||||||
}
|
|
||||||
.text-on-surface {
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
}
|
|
||||||
.text-on-surface-variant {
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
}
|
|
||||||
.text-on-surface-variant\/50 {
|
|
||||||
color: color-mix(in srgb, #424656 50%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
color: color-mix(in oklab, var(--color-on-surface-variant) 50%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.text-primary {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
.text-secondary {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
.text-tertiary {
|
|
||||||
color: var(--color-tertiary);
|
|
||||||
}
|
|
||||||
.text-white {
|
|
||||||
color: var(--color-white);
|
|
||||||
}
|
|
||||||
.uppercase {
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.antialiased {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
.placeholder-on-surface-variant {
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--color-on-surface-variant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter {
|
|
||||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
|
||||||
}
|
|
||||||
.backdrop-blur-md {
|
|
||||||
--tw-backdrop-blur: blur(var(--blur-md));
|
|
||||||
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
|
||||||
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
|
|
||||||
}
|
|
||||||
.transition {
|
|
||||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
|
||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
||||||
}
|
|
||||||
.transition-colors {
|
|
||||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
|
||||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
|
||||||
}
|
|
||||||
.hover\:border-primary\/30 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
border-color: color-mix(in srgb, #0050cb 30%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
border-color: color-mix(in oklab, var(--color-primary) 30%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:bg-surface-container-low {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
background-color: var(--color-surface-container-low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:text-on-surface {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
color: var(--color-on-surface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hover\:text-primary\/50 {
|
|
||||||
&:hover {
|
|
||||||
@media (hover: hover) {
|
|
||||||
color: color-mix(in srgb, #0050cb 50%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
color: color-mix(in oklab, var(--color-primary) 50%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focus\:border-primary\/50 {
|
|
||||||
&:focus {
|
|
||||||
border-color: color-mix(in srgb, #0050cb 50%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
border-color: color-mix(in oklab, var(--color-primary) 50%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focus\:ring-2 {
|
|
||||||
&:focus {
|
|
||||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
|
||||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focus\:ring-primary\/30 {
|
|
||||||
&:focus {
|
|
||||||
--tw-ring-color: color-mix(in srgb, #0050cb 30%, transparent);
|
|
||||||
@supports (color: color-mix(in lab, red, red)) {
|
|
||||||
--tw-ring-color: color-mix(in oklab, var(--color-primary) 30%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.focus\:outline-none {
|
|
||||||
&:focus {
|
|
||||||
--tw-outline-style: none;
|
|
||||||
outline-style: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sm\:grid-cols-2 {
|
|
||||||
@media (width >= 40rem) {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.lg\:grid-cols-3 {
|
|
||||||
@media (width >= 64rem) {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.xl\:grid-cols-4 {
|
|
||||||
@media (width >= 80rem) {
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@property --tw-rotate-x {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-rotate-y {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-rotate-z {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-skew-x {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-skew-y {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-space-y-reverse {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0;
|
|
||||||
}
|
|
||||||
@property --tw-border-style {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: solid;
|
|
||||||
}
|
|
||||||
@property --tw-font-weight {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-tracking {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-blur {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-brightness {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-contrast {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-grayscale {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-hue-rotate {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-invert {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-opacity {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-saturate {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-sepia {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-drop-shadow {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-drop-shadow-color {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-drop-shadow-alpha {
|
|
||||||
syntax: "<percentage>";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 100%;
|
|
||||||
}
|
|
||||||
@property --tw-drop-shadow-size {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-blur {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-brightness {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-contrast {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-grayscale {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-hue-rotate {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-invert {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-opacity {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-saturate {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-backdrop-sepia {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-shadow {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0 0 #0000;
|
|
||||||
}
|
|
||||||
@property --tw-shadow-color {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-shadow-alpha {
|
|
||||||
syntax: "<percentage>";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 100%;
|
|
||||||
}
|
|
||||||
@property --tw-inset-shadow {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0 0 #0000;
|
|
||||||
}
|
|
||||||
@property --tw-inset-shadow-color {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-inset-shadow-alpha {
|
|
||||||
syntax: "<percentage>";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 100%;
|
|
||||||
}
|
|
||||||
@property --tw-ring-color {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-ring-shadow {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0 0 #0000;
|
|
||||||
}
|
|
||||||
@property --tw-inset-ring-color {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-inset-ring-shadow {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0 0 #0000;
|
|
||||||
}
|
|
||||||
@property --tw-ring-inset {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
}
|
|
||||||
@property --tw-ring-offset-width {
|
|
||||||
syntax: "<length>";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0px;
|
|
||||||
}
|
|
||||||
@property --tw-ring-offset-color {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: #fff;
|
|
||||||
}
|
|
||||||
@property --tw-ring-offset-shadow {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: 0 0 #0000;
|
|
||||||
}
|
|
||||||
@layer properties {
|
|
||||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
|
||||||
*, ::before, ::after, ::backdrop {
|
|
||||||
--tw-rotate-x: initial;
|
|
||||||
--tw-rotate-y: initial;
|
|
||||||
--tw-rotate-z: initial;
|
|
||||||
--tw-skew-x: initial;
|
|
||||||
--tw-skew-y: initial;
|
|
||||||
--tw-space-y-reverse: 0;
|
|
||||||
--tw-border-style: solid;
|
|
||||||
--tw-font-weight: initial;
|
|
||||||
--tw-tracking: initial;
|
|
||||||
--tw-blur: initial;
|
|
||||||
--tw-brightness: initial;
|
|
||||||
--tw-contrast: initial;
|
|
||||||
--tw-grayscale: initial;
|
|
||||||
--tw-hue-rotate: initial;
|
|
||||||
--tw-invert: initial;
|
|
||||||
--tw-opacity: initial;
|
|
||||||
--tw-saturate: initial;
|
|
||||||
--tw-sepia: initial;
|
|
||||||
--tw-drop-shadow: initial;
|
|
||||||
--tw-drop-shadow-color: initial;
|
|
||||||
--tw-drop-shadow-alpha: 100%;
|
|
||||||
--tw-drop-shadow-size: initial;
|
|
||||||
--tw-backdrop-blur: initial;
|
|
||||||
--tw-backdrop-brightness: initial;
|
|
||||||
--tw-backdrop-contrast: initial;
|
|
||||||
--tw-backdrop-grayscale: initial;
|
|
||||||
--tw-backdrop-hue-rotate: initial;
|
|
||||||
--tw-backdrop-invert: initial;
|
|
||||||
--tw-backdrop-opacity: initial;
|
|
||||||
--tw-backdrop-saturate: initial;
|
|
||||||
--tw-backdrop-sepia: initial;
|
|
||||||
--tw-shadow: 0 0 #0000;
|
|
||||||
--tw-shadow-color: initial;
|
|
||||||
--tw-shadow-alpha: 100%;
|
|
||||||
--tw-inset-shadow: 0 0 #0000;
|
|
||||||
--tw-inset-shadow-color: initial;
|
|
||||||
--tw-inset-shadow-alpha: 100%;
|
|
||||||
--tw-ring-color: initial;
|
|
||||||
--tw-ring-shadow: 0 0 #0000;
|
|
||||||
--tw-inset-ring-color: initial;
|
|
||||||
--tw-inset-ring-shadow: 0 0 #0000;
|
|
||||||
--tw-ring-inset: initial;
|
|
||||||
--tw-ring-offset-width: 0px;
|
|
||||||
--tw-ring-offset-color: #fff;
|
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
-5
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
@@ -1,85 +0,0 @@
|
|||||||
{{define "host_list"}}
|
|
||||||
<div id="host-list" class="flex flex-col gap-4">
|
|
||||||
<div x-show="view === 'grid'" x-cloak
|
|
||||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
||||||
{{range .Hosts}}
|
|
||||||
<div class="rounded-xl bg-white border border-outline-variant/30 p-4 hover:shadow-md transition-shadow animate-fade-in">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-8 h-8 rounded-lg {{if eq .Status "active"}}bg-primary/10 text-primary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}} flex items-center justify-center">
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-on-surface">{{.Name}}</p>
|
|
||||||
<p class="text-xs text-on-surface-variant">{{.IP}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs font-medium px-2 py-0.5 rounded-full {{if eq .Status "active"}}bg-secondary/10 text-secondary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}}">
|
|
||||||
{{.Status}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 text-xs text-on-surface-variant mb-3">
|
|
||||||
<span>{{.OS}}</span>
|
|
||||||
<span>·</span>
|
|
||||||
<span>{{.Provider}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between pt-3 border-t border-outline-variant/20">
|
|
||||||
<span class="text-xs text-on-surface-variant">{{.LastSeen}}</span>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<button class="p-1.5 rounded-lg hover:bg-primary/10 text-on-surface-variant hover:text-primary transition-colors"
|
|
||||||
onclick="alert('Connect to {{.Name}}')" title="Connect">
|
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
||||||
</button>
|
|
||||||
<button class="p-1.5 rounded-lg hover:bg-red-50 text-on-surface-variant hover:text-red-500 transition-colors"
|
|
||||||
hx-delete="/hosts/{{.ID}}" hx-target="#host-list" hx-confirm="Delete {{.Name}}?" title="Delete">
|
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div x-show="view === 'list'" x-cloak class="flex flex-col gap-2">
|
|
||||||
{{range .Hosts}}
|
|
||||||
<div class="rounded-xl bg-white border border-outline-variant/30 px-4 py-3 flex items-center gap-4 hover:shadow-sm transition-shadow animate-fade-in">
|
|
||||||
<div class="w-2 h-2 rounded-full {{if eq .Status "active"}}bg-secondary{{else}}bg-on-surface-variant{{end}} shrink-0"></div>
|
|
||||||
<div class="w-8 h-8 rounded-lg {{if eq .Status "active"}}bg-primary/10 text-primary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}} flex items-center justify-center shrink-0">
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></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>
|
|
||||||
<span class="text-xs font-medium px-1.5 py-0.5 rounded-full {{if eq .Status "active"}}bg-secondary/10 text-secondary{{else}}bg-on-surface-variant/10 text-on-surface-variant{{end}}">{{.Status}}</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-on-surface-variant truncate">{{.IP}} · {{.OS}} · {{.Provider}}</p>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs text-on-surface-variant shrink-0">{{.LastSeen}}</span>
|
|
||||||
<div class="flex gap-1 shrink-0">
|
|
||||||
<button class="p-1.5 rounded-lg hover:bg-primary/10 text-on-surface-variant hover:text-primary transition-colors"
|
|
||||||
onclick="alert('Connect to {{.Name}}')" title="Connect">
|
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
||||||
</button>
|
|
||||||
<button class="p-1.5 rounded-lg hover:bg-red-50 text-on-surface-variant hover:text-red-500 transition-colors"
|
|
||||||
hx-delete="/hosts/{{.ID}}" hx-target="#host-list" hx-confirm="Delete {{.Name}}?" title="Delete">
|
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if eq (len .Hosts) 0}}
|
|
||||||
<div class="text-center py-12 text-on-surface-variant">
|
|
||||||
<p class="text-lg font-medium">No hosts found</p>
|
|
||||||
<p class="text-sm mt-1">Try a different search or add a new host</p>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<div class="rounded-xl border-2 border-dashed border-outline-variant/30 p-6 flex flex-col items-center justify-center text-on-surface-variant/50 hover:border-primary/30 hover:text-primary/50 transition-colors cursor-pointer min-h-[160px]"
|
|
||||||
@click="$dispatch('open-modal')">
|
|
||||||
<svg class="w-8 h-8 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
|
||||||
<span class="text-sm font-medium">Add New Host</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
{{define "host_modal"}}
|
|
||||||
<div id="host-modal" class="fixed inset-0 z-50" x-data="{ open: false }" x-show="open" x-cloak
|
|
||||||
@open-modal.window="open = true" @keydown.escape.window="open = false">
|
|
||||||
<div class="absolute inset-0 bg-black/30 backdrop-blur-sm" @click="open = false"></div>
|
|
||||||
<div class="absolute inset-0 flex items-center justify-center p-4" x-show="open" x-transition>
|
|
||||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6" @click.outside="open = false">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<h2 class="text-lg font-semibold text-on-surface">Add New Host</h2>
|
|
||||||
<button @click="open = false"
|
|
||||||
class="p-1 rounded-lg hover:bg-surface-container-low text-on-surface-variant">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form hx-post="/hosts" hx-target="#host-list" hx-swap="outerHTML"
|
|
||||||
@submit="open = false"
|
|
||||||
class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-on-surface mb-1">Host Name</label>
|
|
||||||
<input type="text" name="name" required placeholder="e.g. web-prod-01"
|
|
||||||
class="w-full 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 focus:border-primary/50">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-on-surface mb-1">IP Address</label>
|
|
||||||
<input type="text" name="ip" required placeholder="e.g. 10.0.1.100"
|
|
||||||
class="w-full 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 focus:border-primary/50">
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-on-surface mb-1">OS</label>
|
|
||||||
<select name="os" class="w-full 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="Ubuntu 22.04">Ubuntu 22.04</option>
|
|
||||||
<option value="Ubuntu 24.04">Ubuntu 24.04</option>
|
|
||||||
<option value="Debian 12">Debian 12</option>
|
|
||||||
<option value="Alpine 3.19">Alpine 3.19</option>
|
|
||||||
<option value="Fedora 39">Fedora 39</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-on-surface mb-1">Provider</label>
|
|
||||||
<select name="provider" class="w-full 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="AWS US-East">AWS US-East</option>
|
|
||||||
<option value="AWS EU-West">AWS EU-West</option>
|
|
||||||
<option value="DigitalOcean">DigitalOcean</option>
|
|
||||||
<option value="Hetzner">Hetzner</option>
|
|
||||||
<option value="GCP US-Central">GCP US-Central</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-on-surface mb-1">Type</label>
|
|
||||||
<select name="type" class="w-full 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="server">Server</option>
|
|
||||||
<option value="api">API</option>
|
|
||||||
<option value="db">Database</option>
|
|
||||||
<option value="web">Web</option>
|
|
||||||
<option value="edge">Edge</option>
|
|
||||||
<option value="desktop">Desktop</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end gap-3 pt-2">
|
|
||||||
<button type="button" @click="open = 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>
|
|
||||||
<button type="submit"
|
|
||||||
class="px-4 py-2 rounded-lg text-sm font-medium bg-primary text-white hover:bg-primary/90 transition-colors">
|
|
||||||
Add Host
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
{{template "layout" .}}
|
|
||||||
|
|
||||||
{{define "content"}}
|
|
||||||
<div id="dashboard-page" class="max-w-7xl mx-auto">
|
|
||||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
|
||||||
<div class="rounded-xl bg-white border border-outline-variant/30 p-4">
|
|
||||||
<div class="flex items-center gap-2 text-primary mb-1">
|
|
||||||
<span class="w-5 h-5 bg-primary/20 rounded flex items-center justify-center text-xs font-bold">⚡</span>
|
|
||||||
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Active Connections</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-2xl font-bold text-on-surface">{{.Active}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-xl bg-white border border-outline-variant/30 p-4">
|
|
||||||
<div class="flex items-center gap-2 text-secondary mb-1">
|
|
||||||
<span class="w-5 h-5 bg-secondary/20 rounded flex items-center justify-center text-xs font-bold">✓</span>
|
|
||||||
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Transfers Today</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-2xl font-bold text-on-surface">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="rounded-xl bg-white border border-outline-variant/30 p-4">
|
|
||||||
<div class="flex items-center gap-2 text-tertiary mb-1">
|
|
||||||
<span class="w-5 h-5 bg-tertiary/20 rounded flex items-center justify-center text-xs font-bold">#</span>
|
|
||||||
<span class="text-xs font-medium text-on-surface-variant uppercase tracking-wide">Saved Hosts</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-2xl font-bold text-on-surface">{{len .Hosts}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-3 mb-6" x-data="{ view: 'grid' }">
|
|
||||||
<div class="relative flex-1 max-w-md">
|
|
||||||
<input type="search" name="q" placeholder="Search hosts..."
|
|
||||||
hx-get="/hosts?q=..." hx-trigger="keyup delay:200ms"
|
|
||||||
hx-target="#host-list" hx-include="[name='filter']"
|
|
||||||
class="w-full pl-9 pr-3 py-2 rounded-lg border border-outline-variant/50
|
|
||||||
bg-white text-sm text-on-surface placeholder-on-surface-variant
|
|
||||||
focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50">
|
|
||||||
</div>
|
|
||||||
<select name="filter"
|
|
||||||
hx-get="/hosts?filter={value}" hx-trigger="change" hx-target="#host-list"
|
|
||||||
class="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="all">All</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="offline">Offline</option>
|
|
||||||
</select>
|
|
||||||
<div class="flex items-center bg-white rounded-lg border border-outline-variant/50 p-0.5">
|
|
||||||
<button @click="view = 'grid'"
|
|
||||||
:class="view === 'grid' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:text-on-surface'"
|
|
||||||
class="p-1.5 rounded transition-colors">
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/></svg>
|
|
||||||
</button>
|
|
||||||
<button @click="view = 'list'"
|
|
||||||
:class="view === 'list' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:text-on-surface'"
|
|
||||||
class="p-1.5 rounded transition-colors">
|
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{template "host_list" .}}
|
|
||||||
</div>
|
|
||||||
{{template "host_modal" .}}
|
|
||||||
{{end}}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>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">
|
|
||||||
{{block "content" .}}{{end}}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{{define "nav"}}
|
|
||||||
<nav class="w-[260px] min-h-screen bg-white/70 backdrop-blur-md border-r border-outline-variant/30 flex flex-col shrink-0">
|
|
||||||
<div class="p-4 border-b border-outline-variant/20">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-white text-sm font-bold">K</div>
|
|
||||||
<span class="font-bold text-lg text-on-surface">Hostkeeper</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 px-3 py-4 space-y-0.5">
|
|
||||||
{{range .NavItems}}
|
|
||||||
<a href="/{{.ID}}"
|
|
||||||
hx-get="/{{.ID}}" hx-target="#main-content" hx-push-url="true"
|
|
||||||
class="flex items-center gap-3 px-3 py-2 rounded-lg transition-colors text-sm
|
|
||||||
{{if eq $.View .ID}}bg-primary/10 text-primary font-medium
|
|
||||||
{{else}}text-on-surface-variant hover:bg-surface-container-low hover:text-on-surface{{end}}">
|
|
||||||
<span class="w-5 h-5 flex items-center justify-center">{{svg .Icon}}</span>
|
|
||||||
<span>{{.Label}}</span>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div class="px-3 py-3 border-t border-outline-variant/20">
|
|
||||||
<p class="text-xs font-medium text-on-surface-variant uppercase tracking-wide mb-2 px-3">Transfers</p>
|
|
||||||
<div class="px-3 py-2 rounded-lg bg-surface-container-low/50 text-xs text-on-surface-variant flex items-center gap-2">
|
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-on-surface-variant/30"></span>
|
|
||||||
No active transfers
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-4 border-t border-outline-variant/20">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-8 h-8 rounded-full bg-tertiary/20 flex items-center justify-center text-tertiary text-sm font-semibold">U</div>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="text-sm font-medium text-on-surface truncate">User</p>
|
|
||||||
<p class="text-xs text-on-surface-variant truncate">Local</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
{{end}}
|
|
||||||
+101
-419
@@ -1,446 +1,128 @@
|
|||||||
# Hostkeeper V2 — Architecture
|
# Architecture
|
||||||
|
|
||||||
> **Status**: Migrasi React → HTMX
|
## Overview
|
||||||
> **Last Updated**: 2026-07-07
|
|
||||||
> **Dokumen arsitektur baru**: [ARCHITECTURE_HTMX.md](ARCHITECTURE_HTMX.md)
|
|
||||||
> **V1 (existing)**: CLI/TUI SSH/SFTP manager — frozen, no changes.
|
|
||||||
|
|
||||||
---
|
Hostkeeper follows a layered architecture:
|
||||||
|
|
||||||
## ⚠️ Keputusan Arsitektur
|
|
||||||
|
|
||||||
Hostkeeper V2 menggunakan **GoFiber + HTMX**, **bukan React/SPA**.
|
|
||||||
|
|
||||||
Keputusan ini diambil karena:
|
|
||||||
1. Developer lebih nyaman dengan Go daripada JavaScript
|
|
||||||
2. ~90% kode adalah Go + HTML template
|
|
||||||
3. Hanya ~410 baris JS yang diperlukan (xterm.js, clipboard, toggle)
|
|
||||||
4. Build lebih sederhana — tanpa Vite, npm, TypeScript
|
|
||||||
5. Bisa berjalan di browser biasa tanpa Electron
|
|
||||||
|
|
||||||
**Dokumen arsitektur lengkap ada di** [`ARCHITECTURE_HTMX.md`](ARCHITECTURE_HTMX.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Communication Flow
|
|
||||||
|
|
||||||
```
|
```
|
||||||
User interaction (keyboard, mouse)
|
CLI Layer (cmd/hostkeeper/)
|
||||||
│
|
|
|
||||||
▼
|
| calls
|
||||||
React component (event handler)
|
v
|
||||||
│
|
Application Layer (pkg/)
|
||||||
▼
|
|
|
||||||
API client (fetch / WebSocket)
|
| calls
|
||||||
│
|
v
|
||||||
▼
|
Storage Layer (pkg/config/, pkg/storage/)
|
||||||
GoFiber HTTP server (localhost:randomPort)
|
|
|
||||||
│
|
| reads/writes
|
||||||
├── REST handlers → pkg/storage → JSON files
|
v
|
||||||
└── WebSocket handlers → pkg/ssh → remote server
|
File System (~/.config/hostkeeper/)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Directory Structure
|
||||||
|
|
||||||
## 2. Technology Stack
|
|
||||||
|
|
||||||
### Backend (Go)
|
|
||||||
|
|
||||||
| Component | Library | Purpose |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| HTTP server | `gofiber/fiber/v2` | REST API, static file serving |
|
|
||||||
| WebSocket | `gofiber/contrib/websocket` | Terminal streaming, real-time updates |
|
|
||||||
| WebSocket engine | `fasthttp/websocket` | Underlying WS protocol (fork of gorilla) |
|
|
||||||
| SSH client | `golang.org/x/crypto/ssh` | Remote SSH connections (reuse V1 pkg/ssh) |
|
|
||||||
| SFTP client | `github.com/pkg/sftp` | File transfer (reuse V1 pkg/sftp) |
|
|
||||||
| Crypto | `crypto/aes`, `crypto/cipher` | AES-256-GCM encryption (reuse V1 pkg/crypto) |
|
|
||||||
| Storage | Standard `os` + `encoding/json` | JSON file persistence (reuse V1 pkg/storage) |
|
|
||||||
| Config | `github.com/spf13/viper` | App configuration (reuse V1 pkg/config) |
|
|
||||||
| UUID | `github.com/google/uuid` | Unique IDs for entities |
|
|
||||||
| Routers | `github.com/gofiber/fiber/v2` | HTTP routing |
|
|
||||||
| Middleware | GoFiber built-in | CORS, compress, recover, logger |
|
|
||||||
|
|
||||||
### Frontend (React/TypeScript)
|
|
||||||
|
|
||||||
| Component | Library | Purpose |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| Framework | React 19 + TypeScript | UI components |
|
|
||||||
| Build tool | Vite | Fast development + production build |
|
|
||||||
| Terminal | xterm.js + addons | Terminal emulation (fit, web-links, search) |
|
|
||||||
| State | Zustand | Lightweight state management |
|
|
||||||
| Routing | React Router v7 | Screen navigation |
|
|
||||||
| Styling | TailwindCSS + CSS variables | Theming (dark/light/contrast) |
|
|
||||||
| Icons | Lucide React | Consistent icon set |
|
|
||||||
| Notifications | Sonner | Toast notifications |
|
|
||||||
|
|
||||||
### Desktop (Electron)
|
|
||||||
|
|
||||||
| Component | Library | Purpose |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| Shell | Electron 33 | Native desktop wrapper |
|
|
||||||
| Packaging | electron-builder | Build .dmg, .exe, .AppImage |
|
|
||||||
| Auto-update | electron-updater | In-app updates |
|
|
||||||
| Keychain | Node.js safeStorage | OS keychain integration |
|
|
||||||
| IPC | Electron IPC | Go backend <-> Electron communication |
|
|
||||||
|
|
||||||
### Mobile
|
|
||||||
|
|
||||||
| Component | Library | Purpose |
|
|
||||||
|-----------|---------|---------|
|
|
||||||
| Go library | gomobile bind | Compile Go to .aar/.xcframework |
|
|
||||||
| UI shell | React Native WebView | Display web UI on mobile |
|
|
||||||
| Native code | Java (Android) / Swift (iOS) | Minimal WebView wrapper |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Directory Structure
|
|
||||||
|
|
||||||
```
|
```
|
||||||
hostkeeper/
|
cmd/hostkeeper/ — CLI commands (Cobra)
|
||||||
├── cmd/ # V1 CLI (FROZEN)
|
├── main.go — Entry point
|
||||||
│ └── hostkeeper/
|
├── root.go — Root command setup
|
||||||
├── pkg/ # V1 shared packages (FROZEN)
|
├── add.go — Add host command
|
||||||
│ ├── ssh/
|
├── list.go — List hosts command
|
||||||
│ ├── sftp/
|
├── connect.go — Connect command
|
||||||
│ ├── crypto/
|
├── edit.go — Edit host command
|
||||||
│ ├── storage/
|
├── delete.go — Delete host command
|
||||||
│ ├── config/
|
├── export.go — Export command
|
||||||
│ ├── knownhosts/
|
├── import.go — Import command
|
||||||
│ └── tui/
|
├── tui.go — TUI command entry
|
||||||
├── internal/ # V1 internal packages (FROZEN)
|
├── version.go — Version command
|
||||||
│ ├── models/
|
├── completion.go — Shell completion
|
||||||
│ └── errors/
|
└── *_test.go — CLI command tests
|
||||||
├── test/ # V1 tests (FROZEN)
|
|
||||||
│
|
pkg/ — Library code
|
||||||
├── app/ # V2 GUI (NEW)
|
├── config/ — Configuration and data directory
|
||||||
│ ├── backend/ # Go HTTP server
|
├── models/ — Data models (Host, Key, Snippet)
|
||||||
│ │ ├── main.go # Entry point, fiber server setup
|
├── storage/ — File and export/import operations
|
||||||
│ │ ├── go.mod # Go module for backend
|
├── errors/ — Error types
|
||||||
│ │ ├── bridge/ # REST API handlers
|
└── tui/ — TUI implementation (Bubble Tea)
|
||||||
│ │ │ ├── hosts.go
|
|
||||||
│ │ │ ├── keys.go
|
test/ — Test suites
|
||||||
│ │ │ ├── snippets.go
|
├── integration/ — End-to-end integration tests
|
||||||
│ │ │ ├── groups.go
|
└── storage/ — Storage tests
|
||||||
│ │ │ ├── portforward.go
|
|
||||||
│ │ │ ├── sftp.go
|
docs/ — Documentation
|
||||||
│ │ │ ├── config.go
|
├── plans/ — Design and implementation plans
|
||||||
│ │ │ ├── vault.go
|
├── INSTALLATION.md
|
||||||
│ │ │ └── export.go
|
├── USAGE.md
|
||||||
│ │ ├── ws/ # WebSocket handlers
|
└── ARCHITECTURE.md
|
||||||
│ │ │ ├── terminal.go # SSH terminal streaming
|
|
||||||
│ │ │ └── session.go # Session management
|
|
||||||
│ │ └── middleware/ # Custom middleware
|
|
||||||
│ │ └── middleware.go # (optional overrides)
|
|
||||||
│ │
|
|
||||||
│ ├── frontend/ # React web UI
|
|
||||||
│ │ ├── src/
|
|
||||||
│ │ │ ├── components/ # Reusable UI components
|
|
||||||
│ │ │ ├── screens/ # Page-level components
|
|
||||||
│ │ │ ├── hooks/ # Custom React hooks
|
|
||||||
│ │ │ ├── stores/ # Zustand state stores
|
|
||||||
│ │ │ ├── api/ # API client functions
|
|
||||||
│ │ │ └── styles/ # CSS, themes, variables
|
|
||||||
│ │ ├── index.html
|
|
||||||
│ │ ├── vite.config.ts
|
|
||||||
│ │ ├── tailwind.config.js
|
|
||||||
│ │ ├── tsconfig.json
|
|
||||||
│ │ └── package.json
|
|
||||||
│ │
|
|
||||||
│ └── electron/ # Electron shell
|
|
||||||
│ ├── main.ts # Main process
|
|
||||||
│ ├── preload.ts # Preload script (IPC bridge)
|
|
||||||
│ ├── build.ts # Build configuration
|
|
||||||
│ ├── package.json
|
|
||||||
│ └── electron-builder.yml
|
|
||||||
│
|
|
||||||
├── mobile/ # V2 Mobile (NEW)
|
|
||||||
│ ├── android/ # Android WebView app
|
|
||||||
│ │ ├── app/
|
|
||||||
│ │ ├── build.gradle
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── ios/ # iOS WKWebView app
|
|
||||||
│ │ ├── Hostkeeper/
|
|
||||||
│ │ ├── Hostkeeper.xcodeproj
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── gomobile/ # Go mobile compilation
|
|
||||||
│ ├── build.sh
|
|
||||||
│ └── ...
|
|
||||||
│
|
|
||||||
├── scripts/ # Build scripts
|
|
||||||
│ ├── build.sh # Full build (Go + FE + Electron)
|
|
||||||
│ ├── build-go.sh # Go cross-compile
|
|
||||||
│ ├── build-fe.sh # Frontend build
|
|
||||||
│ ├── build-electron.sh # Electron package
|
|
||||||
│ └── build-mobile.sh # Mobile build
|
|
||||||
│
|
|
||||||
├── docs/ # Documentation (V2 planning)
|
|
||||||
│ ├── PROGRESS.md # Status tracker (read this first)
|
|
||||||
│ ├── ARCHITECTURE.md # This file
|
|
||||||
│ ├── API.md # REST API + WebSocket spec
|
|
||||||
│ ├── DATA_MODELS.md # Extended data models
|
|
||||||
│ ├── UI_COMPONENTS.md # React component tree
|
|
||||||
│ ├── UI_TERMIUS_REFERENCE.md # Termius visual reference
|
|
||||||
│ ├── SPRINT_PLAN.md # Sprint-by-sprint task breakdown
|
|
||||||
│ ├── BUILD_SYSTEM.md # Build pipeline
|
|
||||||
│ ├── PERFORMANCE.md # Performance & stability
|
|
||||||
│ └── TIMELINE.md # Project timeline
|
|
||||||
│
|
|
||||||
├── CHANGELOG.md
|
|
||||||
├── README.md
|
|
||||||
├── AGENTS.md # AI session checkpoint
|
|
||||||
├── v1/ # V1 CLI/TUI (FROZEN)
|
|
||||||
│ ├── cmd/hostkeeper/
|
|
||||||
│ ├── internal/
|
|
||||||
│ ├── pkg/
|
|
||||||
│ ├── test/
|
|
||||||
│ ├── docs/ # V1 docs
|
|
||||||
│ ├── go.mod
|
|
||||||
│ └── Makefile
|
|
||||||
├── template/ # React UI template (Lumina System)
|
|
||||||
└── app/ # V2 app (scaffolding not started)
|
|
||||||
├── backend/
|
|
||||||
├── frontend/
|
|
||||||
└── electron/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Key Components
|
||||||
|
|
||||||
## 4. Process Model
|
### Models (`pkg/models/`)
|
||||||
|
|
||||||
### Startup Sequence
|
The core data structures:
|
||||||
|
|
||||||
|
- **Host** — SSH host with address, credentials, group, tags
|
||||||
|
- **Key** — SSH key metadata
|
||||||
|
- **Snippet** — Reusable connection snippets
|
||||||
|
|
||||||
|
### Configuration (`pkg/config/`)
|
||||||
|
|
||||||
|
Manages application configuration, data directory path, and initialization.
|
||||||
|
|
||||||
|
### Storage (`pkg/storage/`)
|
||||||
|
|
||||||
|
Handles persistent storage:
|
||||||
|
- File-based JSON storage per data type
|
||||||
|
- Export/import with replace and merge strategies
|
||||||
|
- File permission enforcement (0600)
|
||||||
|
|
||||||
|
### CLI Layer (`cmd/hostkeeper/`)
|
||||||
|
|
||||||
|
Each command follows a consistent pattern:
|
||||||
|
1. Parse flags
|
||||||
|
2. Load config and storage
|
||||||
|
3. Execute business logic
|
||||||
|
4. Format output
|
||||||
|
|
||||||
|
### TUI (`pkg/tui/`)
|
||||||
|
|
||||||
|
Bubble Tea model with:
|
||||||
|
- Screen-based navigation (extensible for future screens)
|
||||||
|
- Host list view with styled output
|
||||||
|
- Keyboard-driven interaction
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
### Adding a Host
|
||||||
|
|
||||||
```
|
```
|
||||||
1. User double-clicks Hostkeeper.app (Electron)
|
User → hostkeeper add (flags) → parse args → Host model → storage.SaveHost() → JSON file
|
||||||
2. Electron main process starts
|
|
||||||
3. Electron spawns Go binary as child process
|
|
||||||
4. Go binary starts GoFiber server on localhost:randomPort
|
|
||||||
5. Go serves frontend dist (embedded in Electron)
|
|
||||||
6. Electron loads web UI from localhost:PORT
|
|
||||||
7. Web UI renders vault unlock screen
|
|
||||||
8. User enters password -> Go unlocks vault (decrypts data)
|
|
||||||
9. User sees host list screen -> ready to use
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Shutdown Sequence
|
### Connecting to a Host
|
||||||
|
|
||||||
```
|
```
|
||||||
1. User closes window / Cmd+Q
|
User → hostkeeper connect <name> → findHost() by ID → resolve key → exec native SSH or Go SSH
|
||||||
2. Electron sends SIGTERM to Go child process
|
|
||||||
3. Go receives signal -> starts graceful shutdown
|
|
||||||
4. Go disconnects all active SSH sessions
|
|
||||||
5. Go saves any pending state
|
|
||||||
6. Go closes fiber server
|
|
||||||
7. Go exits
|
|
||||||
8. Electron exits
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development Mode
|
### Export/Import
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Terminal: cd app/backend && go run .
|
Export: storage → marshal JSON → write file
|
||||||
2. Terminal: cd app/frontend && npm run dev
|
Import: read file → unmarshal → strategy (merge/replace) → save all
|
||||||
3. Go server at localhost:8080
|
|
||||||
4. Frontend dev server at localhost:5173 (Vite HMR)
|
|
||||||
5. Frontend proxies API requests to localhost:8080
|
|
||||||
6. Open browser at localhost:5173
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Security
|
||||||
|
|
||||||
## 5. Data Flow
|
- Credential files stored with `0600` permissions
|
||||||
|
- SSH key content stored within host credentials
|
||||||
|
- Direct mode uses Go SSH client (no shell, command execution only)
|
||||||
|
- Native SSH mode delegates all terminal handling to system SSH
|
||||||
|
|
||||||
### REST API Request Flow
|
## Future Architecture
|
||||||
|
|
||||||
```
|
- SQLite database for improved query capabilities
|
||||||
React component
|
- Encrypted credential storage (age/gpg)
|
||||||
→ useQuery/useMutation (React Query or manual fetch)
|
- Configuration encryption
|
||||||
→ API client (fetch wrapper)
|
- Plugin system for custom authentication methods
|
||||||
→ HTTP request to GoFiber (localhost:PORT/api/...)
|
|
||||||
→ GoFiber router -> handler function
|
|
||||||
→ Handler calls pkg/storage (read/write JSON)
|
|
||||||
→ Response sent back through the chain
|
|
||||||
```
|
|
||||||
|
|
||||||
### Terminal WebSocket Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User opens terminal tab
|
|
||||||
→ React: XTermWrapper mounts
|
|
||||||
→ XTermWrapper opens WebSocket to ws://localhost:PORT/ws/terminal?host_id=xxx&cols=80&rows=24
|
|
||||||
→ GoFiber WebSocket handler accepts connection
|
|
||||||
→ Go: SSH client connects to remote host (reuse pkg/ssh)
|
|
||||||
→ SSH session created, PTY requested
|
|
||||||
→ SSH stdout goroutine -> reads SSH output -> c.WriteMessage(BinaryMessage, data)
|
|
||||||
-> xterm.js receives data -> renders in terminal
|
|
||||||
-> xterm.js onKey -> c.WriteMessage(BinaryMessage, input)
|
|
||||||
-> Go receives input -> session.Stdin.Write(input) -> SSH remote
|
|
||||||
```
|
|
||||||
|
|
||||||
### SFTP File Transfer Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User drags file to remote pane
|
|
||||||
→ React: FileList onDrop handler
|
|
||||||
→ API client: POST /api/sftp/upload (multipart/form-data)
|
|
||||||
→ Go: bridge/sftp.go receives upload
|
|
||||||
→ Go: pkg/sftp client writes to remote
|
|
||||||
→ Response: { "success": true }
|
|
||||||
→ React: refresh remote file list
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Security Architecture
|
|
||||||
|
|
||||||
### Encryption Model
|
|
||||||
|
|
||||||
| Layer | Method | Implementation |
|
|
||||||
|-------|--------|----------------|
|
|
||||||
| Data at rest | AES-256-GCM | `pkg/crypto/crypto.go` (reuse V1) |
|
|
||||||
| Key derivation | PBKDF2 (100k iterations) | `pkg/crypto/crypto.go` (reuse V1) |
|
|
||||||
| Password storage | SHA-256 hash | `pkg/config/config.go` (reuse V1) |
|
|
||||||
| Known hosts | TOFU verification | `pkg/knownhosts/knownhosts.go` (reuse V1) |
|
|
||||||
| Transport | localhost only | No TLS needed (same machine) |
|
|
||||||
| Export | AES-256-GCM with password | `pkg/storage/json_storage.go` (reuse V1) |
|
|
||||||
|
|
||||||
### Vault Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
App start -> vault locked -> prompt password
|
|
||||||
-> PBKDF2 derive key from password
|
|
||||||
-> Try decrypt hosts.json
|
|
||||||
-> Success -> vault unlocked, data accessible
|
|
||||||
-> Fail -> wrong password, show error
|
|
||||||
-> Vault locked -> no data accessible
|
|
||||||
-> Auto-lock after inactivity timer
|
|
||||||
```
|
|
||||||
|
|
||||||
### OS Keychain Integration
|
|
||||||
|
|
||||||
| Platform | Library | Purpose |
|
|
||||||
|----------|---------|---------|
|
|
||||||
| macOS | `security` CLI or `go-keychain` | Store vault master key |
|
|
||||||
| Windows | `go-credential` | Store vault master key |
|
|
||||||
| Linux | `dbus` Secret Service | Store vault master key |
|
|
||||||
| Electron | `safeStorage` API | Store vault master key |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. WebSocket Protocol
|
|
||||||
|
|
||||||
### Terminal WebSocket
|
|
||||||
|
|
||||||
**Connection**: `ws://localhost:PORT/ws/terminal?host_id=xxx&cols=80&rows=24`
|
|
||||||
|
|
||||||
**Message types** (binary frames):
|
|
||||||
|
|
||||||
| Direction | Type | Content |
|
|
||||||
|-----------|------|---------|
|
|
||||||
| Client → Server | Input | Raw keystroke bytes |
|
|
||||||
| Server → Client | Output | Terminal output bytes |
|
|
||||||
| Client → Server | Resize | JSON: `{ "type": "resize", "cols": 120, "rows": 40 }` |
|
|
||||||
| Server → Client | Status | JSON: `{ "type": "status", "state": "connected" }` |
|
|
||||||
| Server → Client | Error | JSON: `{ "type": "error", "message": "..." }` |
|
|
||||||
|
|
||||||
**Keepalive**: Client sends ping every 30s, server responds with pong.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Build System
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Terminal 1: Go backend
|
|
||||||
cd app/backend
|
|
||||||
go run .
|
|
||||||
|
|
||||||
# Terminal 2: Frontend with hot reload
|
|
||||||
cd app/frontend
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Browser opens at http://localhost:5173
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Full build
|
|
||||||
./scripts/build.sh
|
|
||||||
|
|
||||||
# Output:
|
|
||||||
# dist/hostkeeper-mac-arm64.dmg
|
|
||||||
# dist/hostkeeper-win-x64.exe
|
|
||||||
# dist/hostkeeper-linux-x64.AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cross-Compilation Matrix
|
|
||||||
|
|
||||||
| Platform | GOOS | GOARCH | Output |
|
|
||||||
|----------|------|--------|--------|
|
|
||||||
| macOS ARM64 | darwin | arm64 | hostkeeper-mac-arm64 |
|
|
||||||
| macOS x64 | darwin | amd64 | hostkeeper-mac-x64 |
|
|
||||||
| Windows x64 | windows | amd64 | hostkeeper-win-x64.exe |
|
|
||||||
| Linux x64 | linux | amd64 | hostkeeper-linux-x64 |
|
|
||||||
| Linux ARM64 | linux | arm64 | hostkeeper-linux-arm64 |
|
|
||||||
| Android ARM64 | android | arm64 | hostkeeper-android.aar |
|
|
||||||
| iOS ARM64 | ios | arm64 | hostkeeper-ios.xcframework |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Error Handling
|
|
||||||
|
|
||||||
### Go Backend Errors
|
|
||||||
|
|
||||||
```go
|
|
||||||
// All API errors follow this format:
|
|
||||||
{
|
|
||||||
"error": "Human-readable error message",
|
|
||||||
"code": "MACHINE_READABLE_CODE",
|
|
||||||
"details": "Optional technical details"
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP status codes:
|
|
||||||
// 200 - Success
|
|
||||||
// 201 - Created
|
|
||||||
// 400 - Bad Request (validation error)
|
|
||||||
// 401 - Unauthorized (vault locked)
|
|
||||||
// 404 - Not Found
|
|
||||||
// 409 - Conflict (duplicate)
|
|
||||||
// 500 - Internal Server Error
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend Error Handling
|
|
||||||
|
|
||||||
- Network errors: automatic retry with exponential backoff (3 retries)
|
|
||||||
- API errors: toast notification with error message
|
|
||||||
- WebSocket errors: reconnect button in terminal, auto-reconnect 3 times
|
|
||||||
- Validation errors: inline field-level error messages
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Testing Strategy
|
|
||||||
|
|
||||||
| Layer | Tool | Coverage Target |
|
|
||||||
|-------|------|----------------|
|
|
||||||
| Go unit tests | `go test` | 80%+ |
|
|
||||||
| Go integration tests | `go test -tags=integration` | Key flows |
|
|
||||||
| React unit tests | Vitest + React Testing Library | 70%+ |
|
|
||||||
| React E2E tests | Playwright | Critical flows |
|
|
||||||
| Manual testing | All platforms | Every sprint |
|
|
||||||
|
|
||||||
### Test Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Go backend tests
|
|
||||||
cd app/backend && go test ./...
|
|
||||||
|
|
||||||
# Frontend tests
|
|
||||||
cd app/frontend && npm test
|
|
||||||
|
|
||||||
# Full test suite
|
|
||||||
make test
|
|
||||||
|
|
||||||
# Race condition check
|
|
||||||
go test -race ./app/backend/...
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,385 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
# Hostkeeper V2 — Build System
|
|
||||||
|
|
||||||
> **Status**: Updated untuk HTMX
|
|
||||||
> **Last Updated**: 2026-07-07
|
|
||||||
> **Frontend**: Go HTML template + HTMX + TailwindCSS v4 (tanpa Vite/npm/React)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Build Overview
|
|
||||||
|
|
||||||
Hostkeeper V2 adalah **single Go binary** yang melayani HTML + static files.
|
|
||||||
|
|
||||||
| Platform | Output | Wrapper | Backend |
|
|
||||||
|----------|--------|---------|---------|
|
|
||||||
| macOS (ARM64) | `.dmg` | Electron (opsional) | Go binary |
|
|
||||||
| macOS (x64) | `.dmg` | Electron (opsional) | Go binary |
|
|
||||||
| Windows (x64) | `.exe` installer | Electron (opsional) | Go binary |
|
|
||||||
| Linux (x64) | `.AppImage` | Electron (opsional) | Go binary |
|
|
||||||
| Android (ARM64) | `.aab` | WebView | Go library (.aar) |
|
|
||||||
| iOS (ARM64) | `.ipa` | WKWebView | Go framework (.xcframework) |
|
|
||||||
|
|
||||||
**Catatan**: Tanpa Electron app tetap bisa jalan — tinggal buka `http://localhost:1947` di browser.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Build Pipeline
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
||||||
│ TailwindCSS │ │ Go Build │ │ Platform │
|
|
||||||
│ Build │ → │ (single │ → │ Package │
|
|
||||||
│ (CSS) │ │ binary) │ │ (opsional) │
|
|
||||||
└──────────────┘ └──────────────┘ └──────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 1: CSS Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# TailwindCSS v4 — input.css → output.css
|
|
||||||
npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css
|
|
||||||
|
|
||||||
# Atau via Makefile
|
|
||||||
make css
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Go Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development (hot reload via air)
|
|
||||||
go run .
|
|
||||||
|
|
||||||
# Production (single binary with embedded static)
|
|
||||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o dist/hostkeeper .
|
|
||||||
|
|
||||||
# Cross-compile
|
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o dist/hostkeeper-darwin-arm64 .
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/hostkeeper-linux-amd64 .
|
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dist/hostkeeper-windows-amd64.exe .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Elektron Package (Opsional)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd app/electron
|
|
||||||
npm install
|
|
||||||
npm run build # produces .dmg / .exe / .AppImage
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Development Workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Terminal 1: Go backend (dengan hot reload)
|
|
||||||
cd app/backend
|
|
||||||
go run .
|
|
||||||
|
|
||||||
# Terminal 2: CSS watch
|
|
||||||
cd app/backend
|
|
||||||
npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css --watch
|
|
||||||
|
|
||||||
# Buka browser
|
|
||||||
open http://localhost:1947
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hot Reload Tools
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install air
|
|
||||||
go install github.com/air-verse/air@latest
|
|
||||||
|
|
||||||
# Run
|
|
||||||
air
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Struktur Build Output
|
|
||||||
|
|
||||||
```
|
|
||||||
dist/
|
|
||||||
├── hostkeeper ← Go binary (single file)
|
|
||||||
├── static/ ← Embedded static assets
|
|
||||||
│ ├── css/
|
|
||||||
│ ├── js/
|
|
||||||
│ ├── xterm/
|
|
||||||
│ └── img/
|
|
||||||
└── views/ ← Embedded HTML templates
|
|
||||||
├── layout.html
|
|
||||||
└── partials/
|
|
||||||
```
|
|
||||||
|
|
||||||
GoFiber `embed` package untuk embed static + views:
|
|
||||||
|
|
||||||
```go
|
|
||||||
//go:embed static/* views/*
|
|
||||||
var embedFS embed.FS
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := fiber.New()
|
|
||||||
app.Static("/static", "./static")
|
|
||||||
engine := html.NewFileSystem(http.FS(embedFS), ".html")
|
|
||||||
app.Settings.Views = engine
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Dependencies
|
|
||||||
|
|
||||||
### Go
|
|
||||||
|
|
||||||
```
|
|
||||||
github.com/gofiber/fiber/v2
|
|
||||||
github.com/gofiber/contrib/websocket
|
|
||||||
github.com/gofiber/template/html/v2
|
|
||||||
github.com/fasthttp/websocket
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript (static files — download, bukan npm)
|
|
||||||
|
|
||||||
```
|
|
||||||
static/js/htmx.min.js ← https://unpkg.com/htmx.org@2
|
|
||||||
static/js/alpine.min.js ← https://cdn.jsdelivr.net/npm/alpinejs@3
|
|
||||||
static/xterm/xterm.js ← https://unpkg.com/@xterm/xterm
|
|
||||||
static/xterm/xterm.css ← https://unpkg.com/@xterm/xterm/css
|
|
||||||
```
|
|
||||||
|
|
||||||
### CSS Tooling
|
|
||||||
|
|
||||||
```
|
|
||||||
@tailwindcss/cli ← npx, build time only
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Makefile
|
|
||||||
|
|
||||||
```makefile
|
|
||||||
.PHONY: dev build css clean
|
|
||||||
|
|
||||||
dev:
|
|
||||||
@echo "Starting dev server..."
|
|
||||||
@go run .
|
|
||||||
|
|
||||||
css:
|
|
||||||
npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css
|
|
||||||
|
|
||||||
css-watch:
|
|
||||||
npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css --watch
|
|
||||||
|
|
||||||
build:
|
|
||||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o dist/hostkeeper .
|
|
||||||
|
|
||||||
build-all:
|
|
||||||
GOOS=darwin GOARCH=arm64 go build -o dist/hostkeeper-darwin-arm64 .
|
|
||||||
GOOS=darwin GOARCH=amd64 go build -o dist/hostkeeper-darwin-amd64 .
|
|
||||||
GOOS=linux GOARCH=amd64 go build -o dist/hostkeeper-linux-amd64 .
|
|
||||||
GOOS=windows GOARCH=amd64 go build -o dist/hostkeeper-windows-amd64.exe .
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf dist/
|
|
||||||
```
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
# Hostkeeper V2 — Sprint Plan (HTMX)
|
|
||||||
|
|
||||||
> **Status**: Migrasi React → HTMX
|
|
||||||
> **Last Updated**: 2026-07-07
|
|
||||||
> **Framework**: GoFiber v2 + HTMX + Alpine.js + TailwindCSS v4
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sprint 0 — Setup GoFiber + HTMX
|
|
||||||
|
|
||||||
**Goal**: GoFiber server running, layout HTML renders, TailwindCSS works.
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
| # | Task | Files | Verify |
|
|
||||||
|---|------|-------|--------|
|
|
||||||
| 0.1 | Go module init | `app/backend/go.mod` | `go mod init` clean |
|
|
||||||
| 0.2 | Install GoFiber + deps | `go.mod` | `go get github.com/gofiber/fiber/v2` |
|
|
||||||
| 0.3 | Minimal Fiber server | `app/backend/main.go` | Server starts on :1947 |
|
|
||||||
| 0.4 | Static files serving | `main.go` | `/static/` serves files |
|
|
||||||
| 0.5 | HTML template engine | `main.go` + `views/layout.html` | Template renders |
|
|
||||||
| 0.6 | Download HTMX + Alpine | `static/js/htmx.min.js`, `static/js/alpine.min.js` | JS files exist |
|
|
||||||
| 0.7 | Setup TailwindCSS v4 | `static/css/output.css` | Tailwind classes work |
|
|
||||||
| 0.8 | Custom CSS (Lumina) | `static/css/custom.css` | dot-grid, glassmorphism, keyframes |
|
|
||||||
| 0.9 | Layout template | `views/layout.html` | Sidebar + header + main |
|
|
||||||
| 0.10 | Nav template | `views/partials/nav.html` | Nav items with active state |
|
|
||||||
| 0.11 | Index + routing | `views/index.html` + `handler/dashboard.go` | Dashboard renders |
|
|
||||||
| 0.12 | Health endpoint | `handler/health.go` | `curl :1947/api/health` |
|
|
||||||
|
|
||||||
### Files to Create (Sprint 0)
|
|
||||||
|
|
||||||
```
|
|
||||||
app/backend/
|
|
||||||
├── main.go
|
|
||||||
├── go.mod
|
|
||||||
├── go.sum
|
|
||||||
├── internal/
|
|
||||||
│ ├── handler/
|
|
||||||
│ │ ├── dashboard.go
|
|
||||||
│ │ └── health.go
|
|
||||||
│ └── middleware/
|
|
||||||
│ └── viewdata.go
|
|
||||||
├── static/
|
|
||||||
│ ├── css/
|
|
||||||
│ │ ├── output.css
|
|
||||||
│ │ └── custom.css
|
|
||||||
│ └── js/
|
|
||||||
│ ├── htmx.min.js
|
|
||||||
│ └── alpine.min.js
|
|
||||||
└── views/
|
|
||||||
├── layout.html
|
|
||||||
├── index.html
|
|
||||||
└── partials/
|
|
||||||
└── nav.html
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sprint 1 — Dashboard + Host List
|
|
||||||
|
|
||||||
**Goal**: Dashboard halaman dengan host list, search, filter, add host modal.
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
| # | Task | Files |
|
|
||||||
|---|------|-------|
|
|
||||||
| 1.1 | Model Host | `internal/model/host.go` |
|
|
||||||
| 1.2 | Dashboard handler | `internal/handler/dashboard.go` |
|
|
||||||
| 1.3 | Dashboard template | `views/partials/dashboard.html` |
|
|
||||||
| 1.4 | Host list partial | `views/partials/host_list.html` |
|
|
||||||
| 1.5 | Host card partial | `views/partials/host_card.html` |
|
|
||||||
| 1.6 | Search HTMX | `hx-get="/hosts?q=..." hx-trigger="keyup delay:200ms"` |
|
|
||||||
| 1.7 | Filter status | `hx-get="/hosts?filter=active"` + `<select>` |
|
|
||||||
| 1.8 | View toggle (grid/list) | Alpine `x-data` |
|
|
||||||
| 1.9 | Add host modal | `views/partials/host_modal.html` + Alpine |
|
|
||||||
| 1.10 | POST /hosts | Handler create host |
|
|
||||||
| 1.11 | Background transfers | `views/partials/dashboard.html` |
|
|
||||||
|
|
||||||
### Files to Create (Sprint 1)
|
|
||||||
|
|
||||||
```
|
|
||||||
internal/model/host.go
|
|
||||||
views/partials/dashboard.html
|
|
||||||
views/partials/host_list.html
|
|
||||||
views/partials/host_card.html
|
|
||||||
views/partials/host_modal.html
|
|
||||||
views/partials/toast.html
|
|
||||||
static/js/clipboard.js ← mulai buat
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sprint 2 — Snippets + Keychain
|
|
||||||
|
|
||||||
**Goal**: Snippet library + credential keychain dengan CRUD.
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
| # | Task | Files |
|
|
||||||
|---|------|-------|
|
|
||||||
| 2.1 | Model Snippet | `internal/model/snippet.go` |
|
|
||||||
| 2.2 | Model Keychain | `internal/model/keychain.go` |
|
|
||||||
| 2.3 | Snippets handler | `internal/handler/snippets.go` |
|
|
||||||
| 2.4 | Keychain handler | `internal/handler/keychain.go` |
|
|
||||||
| 2.5 | Snippets template | `views/partials/snippets.html` |
|
|
||||||
| 2.6 | Snippet grid partial | `views/partials/snippet_grid.html` |
|
|
||||||
| 2.7 | Snippet modal | `views/partials/snippet_modal.html` |
|
|
||||||
| 2.8 | Keychain template | `views/partials/keychain.html` |
|
|
||||||
| 2.9 | Key grid partial | `views/partials/key_grid.html` |
|
|
||||||
| 2.10 | Key modal | `views/partials/key_modal.html` |
|
|
||||||
| 2.11 | Clipboard JS | `static/js/clipboard.js` |
|
|
||||||
| 2.12 | Passphrase reveal + strength | `static/js/utils.js` |
|
|
||||||
|
|
||||||
### Files to Create (Sprint 2)
|
|
||||||
|
|
||||||
```
|
|
||||||
internal/model/snippet.go
|
|
||||||
internal/model/keychain.go
|
|
||||||
internal/handler/snippets.go
|
|
||||||
internal/handler/keychain.go
|
|
||||||
views/partials/snippets.html
|
|
||||||
views/partials/snippet_grid.html
|
|
||||||
views/partials/snippet_modal.html
|
|
||||||
views/partials/keychain.html
|
|
||||||
views/partials/key_grid.html
|
|
||||||
views/partials/key_modal.html
|
|
||||||
static/js/clipboard.js
|
|
||||||
static/js/utils.js
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sprint 3 — Settings + Brief Overlay
|
|
||||||
|
|
||||||
**Goal**: Settings page with profile form, toggles, connected devices.
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
| # | Task | Files |
|
|
||||||
|---|------|-------|
|
|
||||||
| 3.1 | Model Device + Config | `internal/model/device.go`, `internal/model/config.go` |
|
|
||||||
| 3.2 | Settings handler | `internal/handler/settings.go` |
|
|
||||||
| 3.3 | Brief handler | `internal/handler/brief.go` |
|
|
||||||
| 3.4 | Settings template | `views/partials/settings.html` |
|
|
||||||
| 3.5 | Brief template | `views/partials/brief.html` |
|
|
||||||
| 3.6 | Toggle switch JS | `static/js/toggles.js` |
|
|
||||||
| 3.7 | Toggle CSS | In custom.css |
|
|
||||||
| 3.8 | Danger zone | `localStorage.clear()` handler |
|
|
||||||
|
|
||||||
### Files to Create (Sprint 3)
|
|
||||||
|
|
||||||
```
|
|
||||||
internal/model/device.go
|
|
||||||
internal/model/config.go
|
|
||||||
internal/handler/settings.go
|
|
||||||
internal/handler/brief.go
|
|
||||||
views/partials/settings.html
|
|
||||||
views/partials/brief.html
|
|
||||||
static/js/toggles.js
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sprint 4 — SFTP Dual-Pane
|
|
||||||
|
|
||||||
**Goal**: File browser dual-pane dengan navigasi folder, upload/download.
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
| # | Task | Files |
|
|
||||||
|---|------|-------|
|
|
||||||
| 4.1 | Model FileItem | `internal/model/file.go` |
|
|
||||||
| 4.2 | Model Transfer | `internal/model/transfer.go` |
|
|
||||||
| 4.3 | SFTP handler | `internal/handler/sftp.go` |
|
|
||||||
| 4.4 | SFTP template | `views/partials/sftp.html` |
|
|
||||||
| 4.5 | SFTP pane partial | `views/partials/sftp_pane.html` |
|
|
||||||
| 4.6 | Breadcrumb navigation | In sftp.html |
|
|
||||||
| 4.7 | Folder create + delete | Modal Alpine + POST |
|
|
||||||
| 4.8 | Toast notification | Alpine `x-data` |
|
|
||||||
|
|
||||||
### Files to Create (Sprint 4)
|
|
||||||
|
|
||||||
```
|
|
||||||
internal/model/file.go
|
|
||||||
internal/model/transfer.go
|
|
||||||
internal/handler/sftp.go
|
|
||||||
views/partials/sftp.html
|
|
||||||
views/partials/sftp_pane.html
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sprint 5 — Terminal + xterm.js
|
|
||||||
|
|
||||||
**Goal**: SSH terminal via WebSocket + xterm.js dengan multi-tab.
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
| # | Task | Files |
|
|
||||||
|---|------|-------|
|
|
||||||
| 5.1 | Download xterm.js | `static/xterm/xterm.js`, `static/xterm/xterm.css` |
|
|
||||||
| 5.2 | Terminal handler | `internal/handler/terminal.go` |
|
|
||||||
| 5.3 | WebSocket handler | `internal/handler/terminal.go` (WS upgrade) |
|
|
||||||
| 5.4 | Terminal template | `views/partials/terminal.html` |
|
|
||||||
| 5.5 | Terminal JS | `static/js/terminal.js` (xterm init + WS) |
|
|
||||||
| 5.6 | Tab management | Alpine `x-data="terminalTabs()"` |
|
|
||||||
| 5.7 | SSH pipe (mock dulu) | Go backend → SSH via v1/pkg/ssh |
|
|
||||||
| 5.8 | Multi-tab per host | Alpine state + multiple xterm instances |
|
|
||||||
|
|
||||||
### Files to Create (Sprint 5)
|
|
||||||
|
|
||||||
```
|
|
||||||
internal/handler/terminal.go
|
|
||||||
views/partials/terminal.html
|
|
||||||
static/js/terminal.js
|
|
||||||
static/xterm/xterm.js
|
|
||||||
static/xterm/xterm.css
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sprint 6 — Data Store Integration
|
|
||||||
|
|
||||||
**Goal**: Ganti mock data dengan real storage dari v1/pkg/. Integrasi SSH, SFTP, crypto.
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
| # | Task | Files |
|
|
||||||
|---|------|-------|
|
|
||||||
| 6.1 | replace directive go.mod | `app/backend/go.mod` |
|
|
||||||
| 6.2 | Integrate v1/pkg/storage | Semua handler |
|
|
||||||
| 6.3 | Integrate v1/pkg/config | `handler/settings.go` |
|
|
||||||
| 6.4 | Integrate v1/pkg/crypto | `handler/keychain.go` |
|
|
||||||
| 6.5 | Integrate v1/pkg/knownhosts | `handler/terminal.go` |
|
|
||||||
| 6.6 | Real SSH via v1/pkg/ssh | `handler/terminal.go` WS |
|
|
||||||
| 6.7 | Real SFTP via v1/pkg/ssh | `handler/sftp.go` |
|
|
||||||
| 6.8 | Remove all mock data | Semua handler |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Total Files
|
|
||||||
|
|
||||||
| Sprint | Go Files | HTML Templates | JS Files | CSS Files |
|
|
||||||
|--------|----------|---------------|----------|-----------|
|
|
||||||
| Sprint 0 | 4 | 3 | 2 | 2 |
|
|
||||||
| Sprint 1 | 2 | 5 | 1 | 0 |
|
|
||||||
| Sprint 2 | 4 | 6 | 2 | 0 |
|
|
||||||
| Sprint 3 | 4 | 2 | 1 | 0 |
|
|
||||||
| Sprint 4 | 3 | 2 | 0 | 0 |
|
|
||||||
| Sprint 5 | 1 | 1 | 1 | 0 |
|
|
||||||
| Sprint 6 | (modify existing) | 0 | 0 | 0 |
|
|
||||||
| **Total** | **18** | **19** | **7** | **2** |
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
# Hostkeeper V2 — Timeline & Milestones (HTMX)
|
|
||||||
|
|
||||||
> **Status**: Updated untuk HTMX
|
|
||||||
> **Last Updated**: 2026-07-07
|
|
||||||
> **Start Date**: 2026-07-07
|
|
||||||
> **Target MVP**: 2026-08-01 (estimasi ~3.5 minggu)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Timeline Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
Minggu 1 │ Sprint 0 ─ Sprint 1 ─ │── Foundation + Dashboard
|
|
||||||
Minggu 2 │ Sprint 2 ─ Sprint 3 ─ │── Snippets + Keychain + Settings
|
|
||||||
Minggu 3 │ Sprint 4 ─────────────│── SFTP
|
|
||||||
Minggu 4 │ Sprint 5 ─ Sprint 6 ─ │── Terminal + Storage Integration
|
|
||||||
└────────────────────────┘
|
|
||||||
7 Jul 14 Jul 21 Jul 28 Jul
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Detailed Timeline
|
|
||||||
|
|
||||||
### Sprint 0 + Sprint 1 — Foundation + Dashboard
|
|
||||||
**5 hari — 7 Jul s.d. 11 Jul**
|
|
||||||
|
|
||||||
| Day | Date | Deliverable |
|
|
||||||
|-----|------|-------------|
|
|
||||||
| 1 | 7 Jul | Go module, Fiber server, static files, HTMX + Alpine |
|
|
||||||
| 2 | 8 Jul | Layout template, nav sidebar, health endpoint |
|
|
||||||
| 3 | 9 Jul | Host model, dashboard handler, host list template |
|
|
||||||
| 4 | 10 Jul | Search HTMX, filter, view toggle, add host modal |
|
|
||||||
| 5 | 11 Jul | Background transfers, toast notification, CSS final |
|
|
||||||
|
|
||||||
**Milestone M1 — Dashboard Functional (11 Jul)**
|
|
||||||
- GoFiber server running on :1947
|
|
||||||
- Layout dengan sidebar navigasi
|
|
||||||
- Dashboard dengan host list, search, filter, add host
|
|
||||||
- HTMX interaktivitas berfungsi
|
|
||||||
|
|
||||||
### Sprint 2 — Snippets + Keychain
|
|
||||||
**3 hari — 14 Jul s.d. 16 Jul**
|
|
||||||
|
|
||||||
| Day | Date | Deliverable |
|
|
||||||
|-----|------|-------------|
|
|
||||||
| 6 | 14 Jul | Snippet model + handler + template + search/filter |
|
|
||||||
| 7 | 15 Jul | Add snippet modal, clipboard JS, tag filter |
|
|
||||||
| 8 | 16 Jul | Keychain model + handler + template + passphrase reveal |
|
|
||||||
|
|
||||||
**Milestone M2 — Snippets + Keys CRUD (16 Jul)**
|
|
||||||
- Snippet library dengan search, tag filter, add modal
|
|
||||||
- Keychain dengan strength badge, passphrase reveal, copy
|
|
||||||
|
|
||||||
### Sprint 3 — Settings + Brief
|
|
||||||
**2 hari — 17 Jul s.d. 18 Jul**
|
|
||||||
|
|
||||||
| Day | Date | Deliverable |
|
|
||||||
|-----|------|-------------|
|
|
||||||
| 9 | 17 Jul | Settings handler + template + toggle switches |
|
|
||||||
| 10 | 18 Jul | Brief overlay, connected devices, danger zone |
|
|
||||||
|
|
||||||
**Milestone M3 — Settings Complete (18 Jul)**
|
|
||||||
- Profile form, toggle switches, connected devices
|
|
||||||
- Brief design system overlay
|
|
||||||
|
|
||||||
### Sprint 4 — SFTP
|
|
||||||
**4 hari — 21 Jul s.d. 24 Jul**
|
|
||||||
|
|
||||||
| Day | Date | Deliverable |
|
|
||||||
|-----|------|-------------|
|
|
||||||
| 11 | 21 Jul | FileItem + Transfer model, SFTP handler |
|
|
||||||
| 12 | 22 Jul | Dual-pane layout, breadcrumb navigation |
|
|
||||||
| 13 | 23 Jul | Folder create/delete, upload/download |
|
|
||||||
| 14 | 24 Jul | Search pane, toast notification, polish |
|
|
||||||
|
|
||||||
**Milestone M4 — SFTP Works (24 Jul)**
|
|
||||||
- Dual-pane file browser dengan navigasi folder
|
|
||||||
- Upload/download, create/delete folder
|
|
||||||
|
|
||||||
### Sprint 5 — Terminal
|
|
||||||
**3 hari — 25 Jul s.d. 28 Jul**
|
|
||||||
|
|
||||||
| Day | Date | Deliverable |
|
|
||||||
|-----|------|-------------|
|
|
||||||
| 15 | 25 Jul | xterm.js setup, WebSocket handler |
|
|
||||||
| 16 | 26 Jul | SSH pipe via v1/pkg/ssh, terminal I/O |
|
|
||||||
| 17 | 28 Jul | Multi-tab Alpine, polish, error handling |
|
|
||||||
|
|
||||||
**Milestone M5 — Terminal Works (28 Jul)**
|
|
||||||
- SSH terminal via WebSocket + xterm.js
|
|
||||||
- Multi-tab support
|
|
||||||
|
|
||||||
### Sprint 6 — Storage Integration + Polish
|
|
||||||
**3 hari — 29 Jul s.d. 31 Jul**
|
|
||||||
|
|
||||||
| Day | Date | Deliverable |
|
|
||||||
|-----|------|-------------|
|
|
||||||
| 18 | 29 Jul | replace directive, v1/pkg/storage integration |
|
|
||||||
| 19 | 30 Jul | v1/pkg/crypto, config, knownhosts integration |
|
|
||||||
| 20 | 31 Jul | Remove mock data, final testing, bug fixes |
|
|
||||||
|
|
||||||
**Milestone MVP — Hostkeeper V2 Ready (31 Jul)**
|
|
||||||
- All features functional with real data persistence
|
|
||||||
- Single Go binary, ready for Electron wrapping
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Milestones
|
|
||||||
|
|
||||||
| Milestone | Date | Deliverable |
|
|
||||||
|-----------|------|-------------|
|
|
||||||
| M1 | 11 Jul | Dashboard + search/filter + add host |
|
|
||||||
| M2 | 16 Jul | Snippets + Keychain CRUD |
|
|
||||||
| M3 | 18 Jul | Settings + Brief overlay |
|
|
||||||
| M4 | 24 Jul | SFTP dual-pane file browser |
|
|
||||||
| M5 | 28 Jul | Terminal SSH via WebSocket |
|
|
||||||
| MVP | 31 Jul | Full integration with real storage |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Estimasi vs React (dulu)
|
|
||||||
|
|
||||||
| Metrik | React (rencana lama) | HTMX (sekarang) |
|
|
||||||
|--------|---------------------|-----------------|
|
|
||||||
| Total waktu | ~6 minggu (30 Jun - 7 Aug) | ~3.5 minggu (7 Jul - 31 Jul) |
|
|
||||||
| Files | ~50 (Go + React + TS + Vite) | ~38 (Go + HTML + JS) |
|
|
||||||
| JS dependencies | 10+ library + npm | 3 file JS (HTMX, Alpine, xterm) |
|
|
||||||
| Build complexity | Vite + TypeScript + npm + Electron | CSS build + Go build |
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
# Hostkeeper V2 — HTMX Components & Partials
|
|
||||||
|
|
||||||
> **Status**: Menggantikan React Component Tree
|
|
||||||
> **Last Updated**: 2026-07-07
|
|
||||||
> **Framework**: Go HTML Template + HTMX + Alpine.js
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Template Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
views/
|
|
||||||
├── layout.html ← Base layout (html, head, body, sidebar, header, scripts)
|
|
||||||
├── index.html ← Entry: {{template "layout" .}} + {{template "content" .}}
|
|
||||||
│
|
|
||||||
└── partials/ ← HTMX-swappable partial fragments
|
|
||||||
├── nav.html
|
|
||||||
├── dashboard.html
|
|
||||||
├── host_list.html
|
|
||||||
├── host_card.html
|
|
||||||
├── host_modal.html
|
|
||||||
├── terminal.html
|
|
||||||
├── sftp.html
|
|
||||||
├── sftp_pane.html
|
|
||||||
├── snippets.html
|
|
||||||
├── snippet_grid.html
|
|
||||||
├── snippet_modal.html
|
|
||||||
├── keychain.html
|
|
||||||
├── key_grid.html
|
|
||||||
├── key_modal.html
|
|
||||||
├── settings.html
|
|
||||||
├── brief.html
|
|
||||||
└── toast.html
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Komponen per Halaman
|
|
||||||
|
|
||||||
### 2.1 Navigasi (layout.html + nav.html)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- layout.html — base shell -->
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
|
||||||
<script src="/static/js/alpine.min.js" defer></script>
|
|
||||||
<script src="/static/js/utils.js"></script>
|
|
||||||
<link rel="stylesheet" href="/static/css/output.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/custom.css">
|
|
||||||
<title>Hostkeeper V2</title>
|
|
||||||
</head>
|
|
||||||
<body class="bg-surface text-on-surface antialiased dot-grid" hx-boost="true">
|
|
||||||
<div class="flex h-screen">
|
|
||||||
{{template "nav" .}}
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
|
||||||
<header class="h-14 border-b border-outline-variant/30 ...">
|
|
||||||
<h1 class="text-lg font-semibold">{{.Title}}</h1>
|
|
||||||
</header>
|
|
||||||
<main id="main-content" class="flex-1 overflow-auto p-6">
|
|
||||||
{{block "content" .}}{{end}}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<!-- nav.html — sidebar -->
|
|
||||||
<nav x-data="{ collapsed: false }"
|
|
||||||
class="w-[260px] ... glass-sidebar border-r border-outline-variant/30 flex flex-col">
|
|
||||||
<div class="p-4 ...">
|
|
||||||
<span class="text-primary font-bold text-lg">Hostkeeper</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 px-3 space-y-1">
|
|
||||||
{{range .NavItems}}
|
|
||||||
<a href="/{{.ID}}"
|
|
||||||
hx-get="/{{.ID}}" hx-target="#main-content" hx-push-url="true"
|
|
||||||
class="flex items-center gap-3 px-3 py-2 rounded-lg
|
|
||||||
{{if eq $.View .ID}}bg-primary/10 text-primary font-medium
|
|
||||||
{{else}}text-on-surface-variant hover:bg-surface-container-low{{end}}">
|
|
||||||
{{svg .Icon}}
|
|
||||||
<span>{{.Label}}</span>
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Dashboard (partials/dashboard.html)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- Container: halaman utama -->
|
|
||||||
<div id="dashboard-page" x-data="{ view: 'grid' }">
|
|
||||||
<!-- Header stats -->
|
|
||||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
|
||||||
<div class="stat-card">...</div>
|
|
||||||
<div class="stat-card">...</div>
|
|
||||||
<div class="stat-card">...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search + Filter + ViewToggle -->
|
|
||||||
<div class="flex items-center gap-3 mb-6">
|
|
||||||
<input type="search" name="q" placeholder="Search hosts..."
|
|
||||||
hx-get="/hosts?q=..." hx-trigger="keyup delay:200ms"
|
|
||||||
hx-target="#host-list" hx-include="[name='filter']"
|
|
||||||
class="...">
|
|
||||||
<select name="filter"
|
|
||||||
hx-get="/hosts?filter={value}" hx-target="#host-list">
|
|
||||||
<option value="all">All</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="offline">Offline</option>
|
|
||||||
</select>
|
|
||||||
<button @click="view = 'grid'" :class="view === 'grid' ? 'bg-primary/10' : ''">
|
|
||||||
{{svg "layout-grid"}}
|
|
||||||
</button>
|
|
||||||
<button @click="view = 'list'" :class="view === 'list' ? 'bg-primary/10' : ''">
|
|
||||||
{{svg "list"}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Host list (swap target) -->
|
|
||||||
<div id="host-list"
|
|
||||||
x-show="view === 'grid'"
|
|
||||||
class="grid grid-cols-4 gap-4">
|
|
||||||
{{template "host_list" .Hosts}}
|
|
||||||
</div>
|
|
||||||
<div x-show="view === 'list'" class="...">
|
|
||||||
{{template "host_list_table" .Hosts}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 Terminal (partials/terminal.html)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="terminal-page" x-data="terminalTabs()">
|
|
||||||
<!-- Tab bar -->
|
|
||||||
<div class="flex items-center gap-1 mb-4">
|
|
||||||
<template x-for="tab in tabs" :key="tab.id">
|
|
||||||
<button @click="switchTab(tab.id)"
|
|
||||||
:class="activeTab === tab.id ? 'bg-primary/10 text-primary' : ''"
|
|
||||||
class="px-3 py-1 rounded text-sm">
|
|
||||||
<span x-text="tab.title"></span>
|
|
||||||
<span @click.stop="closeTab(tab.id)" class="ml-2 cursor-pointer">×</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<button @click="addTab()" class="...">+</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Terminal container -->
|
|
||||||
<div x-ref="terminalContainer"
|
|
||||||
class="bg-[#1e1e1e] rounded-xl p-4 font-mono text-sm min-h-[400px]">
|
|
||||||
<div id="xterm-container" class="..."></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/xterm/xterm.js"></script>
|
|
||||||
<script src="/static/js/terminal.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.4 SFTP (partials/sftp.html)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="sftp-page" x-data="{ toast: '', showToast: false }">
|
|
||||||
<div class="grid grid-cols-2 gap-6">
|
|
||||||
<!-- Local pane -->
|
|
||||||
<div class="pane glass-panel">
|
|
||||||
<div class="flex items-center gap-2 mb-4">
|
|
||||||
{{svg "hard-drive"}}
|
|
||||||
<span class="font-medium">Local Workstation</span>
|
|
||||||
<input type="search" placeholder="Search files..." class="...">
|
|
||||||
</div>
|
|
||||||
<div class="breadcrumb">
|
|
||||||
{{range .LocalBreadcrumb}}
|
|
||||||
<a href="#" hx-get="/sftp/ls?path={{.Path}}"
|
|
||||||
hx-target="#local-pane" class="...">{{.Name}}</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<div id="local-pane" class="file-table">
|
|
||||||
{{template "sftp_pane" .LocalFiles}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Remote pane -->
|
|
||||||
<div class="pane glass-panel">
|
|
||||||
... sama seperti local pane ...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Drop zone -->
|
|
||||||
<div class="drop-zone ...">Drop files here</div>
|
|
||||||
|
|
||||||
<!-- Toast -->
|
|
||||||
<div x-show="showToast"
|
|
||||||
x-text="toast"
|
|
||||||
x-init="$watch('showToast', v => v && setTimeout(() => showToast = false, 3000))"
|
|
||||||
class="toast">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.5 Snippets (partials/snippets.html)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="snippets-page" x-data="{ showForm: false, copiedId: null }">
|
|
||||||
<div class="flex gap-6">
|
|
||||||
<!-- Sidebar filters -->
|
|
||||||
<div class="w-48 space-y-4">
|
|
||||||
<div class="space-y-1">
|
|
||||||
<h3 class="text-sm font-medium text-on-surface-variant">Collections</h3>
|
|
||||||
<a href="#" hx-get="/snippets?col=all" hx-target="#snippet-grid"
|
|
||||||
class="filter-item {{if eq .Collection "all"}}active{{end}}">All</a>
|
|
||||||
<a href="#" hx-get="/snippets?col=favorites" hx-target="#snippet-grid"
|
|
||||||
class="filter-item">Favorites</a>
|
|
||||||
<a href="#" hx-get="/snippets?col=recent" hx-target="#snippet-grid"
|
|
||||||
class="filter-item">Recent</a>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<h3 class="text-sm font-medium text-on-surface-variant">Tags</h3>
|
|
||||||
{{range .AllTags}}
|
|
||||||
<a href="#" hx-get="/snippets?tag={{.}}" hx-target="#snippet-grid"
|
|
||||||
class="filter-item">{{.}}</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main grid -->
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center gap-3 mb-4">
|
|
||||||
<input type="search" placeholder="Search snippets..."
|
|
||||||
hx-get="/snippets?q=..." hx-trigger="keyup delay:200ms"
|
|
||||||
hx-target="#snippet-grid" class="...">
|
|
||||||
<button @click="showForm = true" class="btn-primary">
|
|
||||||
+ Snippet
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="snippet-grid" class="grid grid-cols-2 gap-4">
|
|
||||||
{{template "snippet_grid" .Snippets}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add snippet modal -->
|
|
||||||
<div x-show="showForm" class="modal-backdrop">
|
|
||||||
<div @click.outside="showForm = false" class="modal-content">
|
|
||||||
<form hx-post="/snippets" hx-target="#snippet-grid" @submit="showForm = false">
|
|
||||||
<input name="title" placeholder="Title" required>
|
|
||||||
<input name="description" placeholder="Description">
|
|
||||||
<textarea name="code" placeholder="Code" rows="8" required></textarea>
|
|
||||||
<input name="tags" placeholder="Tags (comma separated)">
|
|
||||||
<button type="submit">Save</button>
|
|
||||||
<button type="button" @click="showForm = false">Cancel</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.6 Keychain (partials/keychain.html)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="keychain-page" x-data="{ showForm: false, revealPass: null }">
|
|
||||||
<div class="flex items-center gap-3 mb-6">
|
|
||||||
<input type="search" placeholder="Search credentials..."
|
|
||||||
hx-get="/keys?q=..." hx-trigger="keyup delay:200ms"
|
|
||||||
hx-target="#key-grid" class="...">
|
|
||||||
<button @click="showForm = true" class="btn-primary">+ Credential</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="key-grid" class="grid grid-cols-2 gap-4">
|
|
||||||
{{range .Keys}}
|
|
||||||
<div class="card glass-panel p-4">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
{{svg "key"}}
|
|
||||||
<span class="font-medium">{{.Name}}</span>
|
|
||||||
</div>
|
|
||||||
<span class="strength-badge-{{.Strength}}">{{.Strength}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1 text-sm">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-on-surface-variant">User</span>
|
|
||||||
<span>{{.User}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-on-surface-variant">Passphrase</span>
|
|
||||||
<div x-data="{ reveal: false }" class="flex items-center gap-2">
|
|
||||||
<span x-text="reveal ? '{{.Passphrase}}' : '••••••••'"></span>
|
|
||||||
<button @click="reveal = !reveal">
|
|
||||||
{{svg "eye"}}
|
|
||||||
</button>
|
|
||||||
<button onclick='copyToClipboard("{{.Passphrase}}")'>
|
|
||||||
{{svg "copy"}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span class="text-on-surface-variant">{{.Fingerprint}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.7 Settings (partials/settings.html)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="settings-page">
|
|
||||||
<div class="max-w-2xl space-y-8">
|
|
||||||
<!-- Profile form -->
|
|
||||||
<div class="card glass-panel p-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4">Profile</h2>
|
|
||||||
<form hx-post="/settings" hx-target="#settings-form" class="space-y-4">
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label>Name</label>
|
|
||||||
<input name="name" value="{{.Profile.Name}}" class="...">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Email</label>
|
|
||||||
<input name="email" value="{{.Profile.Email}}" class="...">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn-primary">Save</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toggles -->
|
|
||||||
<div class="card glass-panel p-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4">Security & Transmission</h2>
|
|
||||||
{{range .Toggles}}
|
|
||||||
<div class="flex items-center justify-between py-3">
|
|
||||||
<div>
|
|
||||||
<span class="font-medium">{{.Label}}</span>
|
|
||||||
<p class="text-sm text-on-surface-variant">{{.Desc}}</p>
|
|
||||||
</div>
|
|
||||||
<button class="toggle-switch {{if .Enabled}}active{{end}}"
|
|
||||||
data-key="{{.Key}}"
|
|
||||||
onclick="toggleSwitch(this)"
|
|
||||||
aria-checked="{{.Enabled}}">
|
|
||||||
<span class="toggle-knob"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Connected devices -->
|
|
||||||
<div class="card glass-panel p-6">
|
|
||||||
<h2 class="text-lg font-semibold mb-4">Connected Devices</h2>
|
|
||||||
{{range .Devices}}
|
|
||||||
<div class="flex items-center gap-3 py-2">
|
|
||||||
{{svg .Type}}
|
|
||||||
<span>{{.Name}}</span>
|
|
||||||
<span class="ml-auto text-sm {{if eq .Status "online"}}text-secondary{{end}}">
|
|
||||||
{{.Status}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Danger zone -->
|
|
||||||
<div class="card border border-red-400/30 bg-red-50 p-6">
|
|
||||||
<h2 class="text-lg font-semibold text-red-600 mb-4">Danger Zone</h2>
|
|
||||||
<p class="text-sm text-red-600 mb-4">Flush all app state — this cannot be undone.</p>
|
|
||||||
<button onclick="if(confirm('Yakin?')){localStorage.clear();location.reload()}"
|
|
||||||
class="btn-danger">
|
|
||||||
Flush All App State
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.8 Brief Overlay (partials/brief.html)
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- Brief overlay — static design system reference -->
|
|
||||||
<div x-data="{ open: false }">
|
|
||||||
<button @click="open = true" class="...">
|
|
||||||
{{svg "file-question"}}
|
|
||||||
</button>
|
|
||||||
<div x-show="open" class="fixed inset-0 z-50">
|
|
||||||
<div class="absolute inset-0 bg-black/20" @click="open = false"></div>
|
|
||||||
<div class="absolute right-0 top-0 h-full w-[400px] glass-sidebar p-6 overflow-auto">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<h2 class="text-lg font-semibold">System Brief</h2>
|
|
||||||
<button @click="open = false">{{svg "x"}}</button>
|
|
||||||
</div>
|
|
||||||
<!-- Static content dari template BriefOverlay.tsx -->
|
|
||||||
<div class="space-y-6">
|
|
||||||
<section>
|
|
||||||
<h3 class="font-medium text-primary mb-2">Color Palette</h3>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<div class="h-12 rounded-lg bg-primary"></div>
|
|
||||||
<div class="h-12 rounded-lg bg-secondary"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h3 class="font-medium text-primary mb-2">Typography</h3>
|
|
||||||
<p class="font-inter text-lg">Inter — Interface UI</p>
|
|
||||||
<p class="font-mono text-sm">JetBrains Mono — Code & Data</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h3 class="font-medium text-primary mb-2">Glassmorphism</h3>
|
|
||||||
<p>backdrop-blur-md + rgba(255,255,255,0.7)</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Alpine.js Components (State Management)
|
|
||||||
|
|
||||||
### 3.1 Terminal Tabs
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// inline dalam terminal.html
|
|
||||||
function terminalTabs() {
|
|
||||||
return {
|
|
||||||
tabs: [{ id: '1', title: 'localhost' }],
|
|
||||||
activeTab: '1',
|
|
||||||
addTab() {
|
|
||||||
const id = String(Date.now());
|
|
||||||
this.tabs.push({ id, title: `host-${this.tabs.length + 1}` });
|
|
||||||
this.activeTab = id;
|
|
||||||
this.$nextTick(() => initTerminal(id));
|
|
||||||
},
|
|
||||||
closeTab(id) {
|
|
||||||
this.tabs = this.tabs.filter(t => t.id !== id);
|
|
||||||
if (this.activeTab === id) this.activeTab = this.tabs[0]?.id;
|
|
||||||
},
|
|
||||||
switchTab(id) {
|
|
||||||
this.activeTab = id;
|
|
||||||
this.$nextTick(() => focusTerminal(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Toast Notifications
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// inline dalam sftp.html, dashboard.html
|
|
||||||
function toastManager() {
|
|
||||||
return {
|
|
||||||
message: '',
|
|
||||||
visible: false,
|
|
||||||
show(msg) {
|
|
||||||
this.message = msg;
|
|
||||||
this.visible = true;
|
|
||||||
setTimeout(() => this.visible = false, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. JavaScript Files (Vanilla)
|
|
||||||
|
|
||||||
### static/js/clipboard.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function copyToClipboard(text, triggerId) {
|
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
|
||||||
// trigger toast via htmx event
|
|
||||||
document.body.dispatchEvent(new CustomEvent('showToast', {
|
|
||||||
detail: { message: 'Copied!' }
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### static/js/toggles.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function toggleSwitch(el) {
|
|
||||||
const checked = el.getAttribute('aria-checked') === 'true';
|
|
||||||
el.setAttribute('aria-checked', !checked);
|
|
||||||
el.classList.toggle('active');
|
|
||||||
fetch(`/settings/toggle?key=${el.dataset.key}&val=${!checked}`, {
|
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### static/js/utils.js
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
function revealPassphrase(el) {
|
|
||||||
const span = el.closest('.pass-row').querySelector('.pass-text');
|
|
||||||
const isHidden = span.dataset.hidden === 'true';
|
|
||||||
span.textContent = isHidden ? span.dataset.value : '••••••••';
|
|
||||||
span.dataset.hidden = isHidden ? 'false' : 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
function strengthClass(len) {
|
|
||||||
if (len > 12) return 'secure';
|
|
||||||
if (len > 7) return 'moderate';
|
|
||||||
return 'weak';
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
# Hostkeeper V2 — Architecture
|
||||||
|
|
||||||
|
> **Status**: V2 Planning Complete
|
||||||
|
> **Last Updated**: 2026-06-29
|
||||||
|
> **V1 (existing)**: CLI/TUI SSH/SFTP manager — frozen, no changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. System Overview
|
||||||
|
|
||||||
|
Hostkeeper V2 is a Termius-like cross-platform SSH/SFTP management GUI. The architecture follows a **Go backend + Web UI + Electron/WebView** pattern — the same approach used by Termius, VS Code, Discord, Slack, and Figma.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ ELECTRON / WEBVIEW │
|
||||||
|
│ Native window, menu bar, tray icon, auto-updater │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ WEB UI (React + TypeScript) │ │
|
||||||
|
│ │ Host List │ Terminal │ SFTP │ Keys │ Settings │ │
|
||||||
|
│ │ ┌───────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ xterm.js (terminal) │ │ │
|
||||||
|
│ │ └───────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
│ │ HTTP + WebSocket │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ GO BACKEND (GoFiber v2) │ │
|
||||||
|
│ │ REST API │ WebSocket │ SSH │ SFTP │ │
|
||||||
|
│ │ ┌───────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ pkg/ssh │ pkg/sftp │ pkg/crypto │ ... │ │ │
|
||||||
|
│ │ └───────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
│ Distributed as:
|
||||||
|
├── .dmg (macOS)
|
||||||
|
├── .exe (Windows)
|
||||||
|
├── .AppImage (Linux)
|
||||||
|
└── .aab / .ipa (Android / iOS)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Communication Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User interaction (keyboard, mouse)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
React component (event handler)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
API client (fetch / WebSocket)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
GoFiber HTTP server (localhost:randomPort)
|
||||||
|
│
|
||||||
|
├── REST handlers → pkg/storage → JSON files
|
||||||
|
└── WebSocket handlers → pkg/ssh → remote server
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Technology Stack
|
||||||
|
|
||||||
|
### Backend (Go)
|
||||||
|
|
||||||
|
| Component | Library | Purpose |
|
||||||
|
|-----------|---------|---------|
|
||||||
|
| HTTP server | `gofiber/fiber/v2` | REST API, static file serving |
|
||||||
|
| WebSocket | `gofiber/contrib/websocket` | Terminal streaming, real-time updates |
|
||||||
|
| WebSocket engine | `fasthttp/websocket` | Underlying WS protocol (fork of gorilla) |
|
||||||
|
| SSH client | `golang.org/x/crypto/ssh` | Remote SSH connections (reuse V1 pkg/ssh) |
|
||||||
|
| SFTP client | `github.com/pkg/sftp` | File transfer (reuse V1 pkg/sftp) |
|
||||||
|
| Crypto | `crypto/aes`, `crypto/cipher` | AES-256-GCM encryption (reuse V1 pkg/crypto) |
|
||||||
|
| Storage | Standard `os` + `encoding/json` | JSON file persistence (reuse V1 pkg/storage) |
|
||||||
|
| Config | `github.com/spf13/viper` | App configuration (reuse V1 pkg/config) |
|
||||||
|
| UUID | `github.com/google/uuid` | Unique IDs for entities |
|
||||||
|
| Routers | `github.com/gofiber/fiber/v2` | HTTP routing |
|
||||||
|
| Middleware | GoFiber built-in | CORS, compress, recover, logger |
|
||||||
|
|
||||||
|
### Frontend (React/TypeScript)
|
||||||
|
|
||||||
|
| Component | Library | Purpose |
|
||||||
|
|-----------|---------|---------|
|
||||||
|
| Framework | React 19 + TypeScript | UI components |
|
||||||
|
| Build tool | Vite | Fast development + production build |
|
||||||
|
| Terminal | xterm.js + addons | Terminal emulation (fit, web-links, search) |
|
||||||
|
| State | Zustand | Lightweight state management |
|
||||||
|
| Routing | React Router v7 | Screen navigation |
|
||||||
|
| Styling | TailwindCSS + CSS variables | Theming (dark/light/contrast) |
|
||||||
|
| Icons | Lucide React | Consistent icon set |
|
||||||
|
| Notifications | Sonner | Toast notifications |
|
||||||
|
|
||||||
|
### Desktop (Electron)
|
||||||
|
|
||||||
|
| Component | Library | Purpose |
|
||||||
|
|-----------|---------|---------|
|
||||||
|
| Shell | Electron 33 | Native desktop wrapper |
|
||||||
|
| Packaging | electron-builder | Build .dmg, .exe, .AppImage |
|
||||||
|
| Auto-update | electron-updater | In-app updates |
|
||||||
|
| Keychain | Node.js safeStorage | OS keychain integration |
|
||||||
|
| IPC | Electron IPC | Go backend <-> Electron communication |
|
||||||
|
|
||||||
|
### Mobile
|
||||||
|
|
||||||
|
| Component | Library | Purpose |
|
||||||
|
|-----------|---------|---------|
|
||||||
|
| Go library | gomobile bind | Compile Go to .aar/.xcframework |
|
||||||
|
| UI shell | React Native WebView | Display web UI on mobile |
|
||||||
|
| Native code | Java (Android) / Swift (iOS) | Minimal WebView wrapper |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
hostkeeper/
|
||||||
|
├── cmd/ # V1 CLI (FROZEN)
|
||||||
|
│ └── hostkeeper/
|
||||||
|
├── pkg/ # V1 shared packages (FROZEN)
|
||||||
|
│ ├── ssh/
|
||||||
|
│ ├── sftp/
|
||||||
|
│ ├── crypto/
|
||||||
|
│ ├── storage/
|
||||||
|
│ ├── config/
|
||||||
|
│ ├── knownhosts/
|
||||||
|
│ └── tui/
|
||||||
|
├── internal/ # V1 internal packages (FROZEN)
|
||||||
|
│ ├── models/
|
||||||
|
│ └── errors/
|
||||||
|
├── test/ # V1 tests (FROZEN)
|
||||||
|
│
|
||||||
|
├── app/ # V2 GUI (NEW)
|
||||||
|
│ ├── backend/ # Go HTTP server
|
||||||
|
│ │ ├── main.go # Entry point, fiber server setup
|
||||||
|
│ │ ├── go.mod # Go module for backend
|
||||||
|
│ │ ├── bridge/ # REST API handlers
|
||||||
|
│ │ │ ├── hosts.go
|
||||||
|
│ │ │ ├── keys.go
|
||||||
|
│ │ │ ├── snippets.go
|
||||||
|
│ │ │ ├── groups.go
|
||||||
|
│ │ │ ├── portforward.go
|
||||||
|
│ │ │ ├── sftp.go
|
||||||
|
│ │ │ ├── config.go
|
||||||
|
│ │ │ ├── vault.go
|
||||||
|
│ │ │ └── export.go
|
||||||
|
│ │ ├── ws/ # WebSocket handlers
|
||||||
|
│ │ │ ├── terminal.go # SSH terminal streaming
|
||||||
|
│ │ │ └── session.go # Session management
|
||||||
|
│ │ └── middleware/ # Custom middleware
|
||||||
|
│ │ └── middleware.go # (optional overrides)
|
||||||
|
│ │
|
||||||
|
│ ├── frontend/ # React web UI
|
||||||
|
│ │ ├── src/
|
||||||
|
│ │ │ ├── components/ # Reusable UI components
|
||||||
|
│ │ │ ├── screens/ # Page-level components
|
||||||
|
│ │ │ ├── hooks/ # Custom React hooks
|
||||||
|
│ │ │ ├── stores/ # Zustand state stores
|
||||||
|
│ │ │ ├── api/ # API client functions
|
||||||
|
│ │ │ └── styles/ # CSS, themes, variables
|
||||||
|
│ │ ├── index.html
|
||||||
|
│ │ ├── vite.config.ts
|
||||||
|
│ │ ├── tailwind.config.js
|
||||||
|
│ │ ├── tsconfig.json
|
||||||
|
│ │ └── package.json
|
||||||
|
│ │
|
||||||
|
│ └── electron/ # Electron shell
|
||||||
|
│ ├── main.ts # Main process
|
||||||
|
│ ├── preload.ts # Preload script (IPC bridge)
|
||||||
|
│ ├── build.ts # Build configuration
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── electron-builder.yml
|
||||||
|
│
|
||||||
|
├── mobile/ # V2 Mobile (NEW)
|
||||||
|
│ ├── android/ # Android WebView app
|
||||||
|
│ │ ├── app/
|
||||||
|
│ │ ├── build.gradle
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── ios/ # iOS WKWebView app
|
||||||
|
│ │ ├── Hostkeeper/
|
||||||
|
│ │ ├── Hostkeeper.xcodeproj
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── gomobile/ # Go mobile compilation
|
||||||
|
│ ├── build.sh
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── scripts/ # Build scripts
|
||||||
|
│ ├── build.sh # Full build (Go + FE + Electron)
|
||||||
|
│ ├── build-go.sh # Go cross-compile
|
||||||
|
│ ├── build-fe.sh # Frontend build
|
||||||
|
│ ├── build-electron.sh # Electron package
|
||||||
|
│ └── build-mobile.sh # Mobile build
|
||||||
|
│
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ ├── v2/ # V2 planning docs
|
||||||
|
│ │ ├── PROGRESS.md # Status tracker (read this first)
|
||||||
|
│ │ ├── ARCHITECTURE.md # This file
|
||||||
|
│ │ ├── API.md # REST API + WebSocket spec
|
||||||
|
│ │ ├── DATA_MODELS.md # Extended data models
|
||||||
|
│ │ ├── UI_COMPONENTS.md # React component tree
|
||||||
|
│ │ ├── UI_TERMIUS_REFERENCE.md # Termius visual reference
|
||||||
|
│ │ ├── SPRINT_PLAN.md # Sprint-by-sprint task breakdown
|
||||||
|
│ │ ├── BUILD_SYSTEM.md # Build pipeline
|
||||||
|
│ │ └── PERFORMANCE.md # Performance & stability
|
||||||
|
│ ├── v1/ # V1 docs (FROZEN, reference only)
|
||||||
|
│ │ ├── ARCHITECTURE.md
|
||||||
|
│ │ ├── TEST_PLAN.md
|
||||||
|
│ │ ├── INSTALLATION.md
|
||||||
|
│ │ └── USAGE.md
|
||||||
|
│ └── plans/ # V1 design docs (FROZEN)
|
||||||
|
│
|
||||||
|
├── go.mod # V1 Go module
|
||||||
|
├── go.sum
|
||||||
|
├── Makefile
|
||||||
|
├── build.sh
|
||||||
|
├── CHANGELOG.md
|
||||||
|
├── PROJECT_STATE.md # V1 project state
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Process Model
|
||||||
|
|
||||||
|
### Startup Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User double-clicks Hostkeeper.app (Electron)
|
||||||
|
2. Electron main process starts
|
||||||
|
3. Electron spawns Go binary as child process
|
||||||
|
4. Go binary starts GoFiber server on localhost:randomPort
|
||||||
|
5. Go serves frontend dist (embedded in Electron)
|
||||||
|
6. Electron loads web UI from localhost:PORT
|
||||||
|
7. Web UI renders vault unlock screen
|
||||||
|
8. User enters password -> Go unlocks vault (decrypts data)
|
||||||
|
9. User sees host list screen -> ready to use
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shutdown Sequence
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User closes window / Cmd+Q
|
||||||
|
2. Electron sends SIGTERM to Go child process
|
||||||
|
3. Go receives signal -> starts graceful shutdown
|
||||||
|
4. Go disconnects all active SSH sessions
|
||||||
|
5. Go saves any pending state
|
||||||
|
6. Go closes fiber server
|
||||||
|
7. Go exits
|
||||||
|
8. Electron exits
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Terminal: cd app/backend && go run .
|
||||||
|
2. Terminal: cd app/frontend && npm run dev
|
||||||
|
3. Go server at localhost:8080
|
||||||
|
4. Frontend dev server at localhost:5173 (Vite HMR)
|
||||||
|
5. Frontend proxies API requests to localhost:8080
|
||||||
|
6. Open browser at localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Data Flow
|
||||||
|
|
||||||
|
### REST API Request Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
React component
|
||||||
|
→ useQuery/useMutation (React Query or manual fetch)
|
||||||
|
→ API client (fetch wrapper)
|
||||||
|
→ HTTP request to GoFiber (localhost:PORT/api/...)
|
||||||
|
→ GoFiber router -> handler function
|
||||||
|
→ Handler calls pkg/storage (read/write JSON)
|
||||||
|
→ Response sent back through the chain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal WebSocket Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User opens terminal tab
|
||||||
|
→ React: XTermWrapper mounts
|
||||||
|
→ XTermWrapper opens WebSocket to ws://localhost:PORT/ws/terminal?host_id=xxx&cols=80&rows=24
|
||||||
|
→ GoFiber WebSocket handler accepts connection
|
||||||
|
→ Go: SSH client connects to remote host (reuse pkg/ssh)
|
||||||
|
→ SSH session created, PTY requested
|
||||||
|
→ SSH stdout goroutine -> reads SSH output -> c.WriteMessage(BinaryMessage, data)
|
||||||
|
-> xterm.js receives data -> renders in terminal
|
||||||
|
-> xterm.js onKey -> c.WriteMessage(BinaryMessage, input)
|
||||||
|
-> Go receives input -> session.Stdin.Write(input) -> SSH remote
|
||||||
|
```
|
||||||
|
|
||||||
|
### SFTP File Transfer Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User drags file to remote pane
|
||||||
|
→ React: FileList onDrop handler
|
||||||
|
→ API client: POST /api/sftp/upload (multipart/form-data)
|
||||||
|
→ Go: bridge/sftp.go receives upload
|
||||||
|
→ Go: pkg/sftp client writes to remote
|
||||||
|
→ Response: { "success": true }
|
||||||
|
→ React: refresh remote file list
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Security Architecture
|
||||||
|
|
||||||
|
### Encryption Model
|
||||||
|
|
||||||
|
| Layer | Method | Implementation |
|
||||||
|
|-------|--------|----------------|
|
||||||
|
| Data at rest | AES-256-GCM | `pkg/crypto/crypto.go` (reuse V1) |
|
||||||
|
| Key derivation | PBKDF2 (100k iterations) | `pkg/crypto/crypto.go` (reuse V1) |
|
||||||
|
| Password storage | SHA-256 hash | `pkg/config/config.go` (reuse V1) |
|
||||||
|
| Known hosts | TOFU verification | `pkg/knownhosts/knownhosts.go` (reuse V1) |
|
||||||
|
| Transport | localhost only | No TLS needed (same machine) |
|
||||||
|
| Export | AES-256-GCM with password | `pkg/storage/json_storage.go` (reuse V1) |
|
||||||
|
|
||||||
|
### Vault Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
App start -> vault locked -> prompt password
|
||||||
|
-> PBKDF2 derive key from password
|
||||||
|
-> Try decrypt hosts.json
|
||||||
|
-> Success -> vault unlocked, data accessible
|
||||||
|
-> Fail -> wrong password, show error
|
||||||
|
-> Vault locked -> no data accessible
|
||||||
|
-> Auto-lock after inactivity timer
|
||||||
|
```
|
||||||
|
|
||||||
|
### OS Keychain Integration
|
||||||
|
|
||||||
|
| Platform | Library | Purpose |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| macOS | `security` CLI or `go-keychain` | Store vault master key |
|
||||||
|
| Windows | `go-credential` | Store vault master key |
|
||||||
|
| Linux | `dbus` Secret Service | Store vault master key |
|
||||||
|
| Electron | `safeStorage` API | Store vault master key |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. WebSocket Protocol
|
||||||
|
|
||||||
|
### Terminal WebSocket
|
||||||
|
|
||||||
|
**Connection**: `ws://localhost:PORT/ws/terminal?host_id=xxx&cols=80&rows=24`
|
||||||
|
|
||||||
|
**Message types** (binary frames):
|
||||||
|
|
||||||
|
| Direction | Type | Content |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| Client → Server | Input | Raw keystroke bytes |
|
||||||
|
| Server → Client | Output | Terminal output bytes |
|
||||||
|
| Client → Server | Resize | JSON: `{ "type": "resize", "cols": 120, "rows": 40 }` |
|
||||||
|
| Server → Client | Status | JSON: `{ "type": "status", "state": "connected" }` |
|
||||||
|
| Server → Client | Error | JSON: `{ "type": "error", "message": "..." }` |
|
||||||
|
|
||||||
|
**Keepalive**: Client sends ping every 30s, server responds with pong.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Build System
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Go backend
|
||||||
|
cd app/backend
|
||||||
|
go run .
|
||||||
|
|
||||||
|
# Terminal 2: Frontend with hot reload
|
||||||
|
cd app/frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Browser opens at http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full build
|
||||||
|
./scripts/build.sh
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# dist/hostkeeper-mac-arm64.dmg
|
||||||
|
# dist/hostkeeper-win-x64.exe
|
||||||
|
# dist/hostkeeper-linux-x64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-Compilation Matrix
|
||||||
|
|
||||||
|
| Platform | GOOS | GOARCH | Output |
|
||||||
|
|----------|------|--------|--------|
|
||||||
|
| macOS ARM64 | darwin | arm64 | hostkeeper-mac-arm64 |
|
||||||
|
| macOS x64 | darwin | amd64 | hostkeeper-mac-x64 |
|
||||||
|
| Windows x64 | windows | amd64 | hostkeeper-win-x64.exe |
|
||||||
|
| Linux x64 | linux | amd64 | hostkeeper-linux-x64 |
|
||||||
|
| Linux ARM64 | linux | arm64 | hostkeeper-linux-arm64 |
|
||||||
|
| Android ARM64 | android | arm64 | hostkeeper-android.aar |
|
||||||
|
| iOS ARM64 | ios | arm64 | hostkeeper-ios.xcframework |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Error Handling
|
||||||
|
|
||||||
|
### Go Backend Errors
|
||||||
|
|
||||||
|
```go
|
||||||
|
// All API errors follow this format:
|
||||||
|
{
|
||||||
|
"error": "Human-readable error message",
|
||||||
|
"code": "MACHINE_READABLE_CODE",
|
||||||
|
"details": "Optional technical details"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP status codes:
|
||||||
|
// 200 - Success
|
||||||
|
// 201 - Created
|
||||||
|
// 400 - Bad Request (validation error)
|
||||||
|
// 401 - Unauthorized (vault locked)
|
||||||
|
// 404 - Not Found
|
||||||
|
// 409 - Conflict (duplicate)
|
||||||
|
// 500 - Internal Server Error
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Error Handling
|
||||||
|
|
||||||
|
- Network errors: automatic retry with exponential backoff (3 retries)
|
||||||
|
- API errors: toast notification with error message
|
||||||
|
- WebSocket errors: reconnect button in terminal, auto-reconnect 3 times
|
||||||
|
- Validation errors: inline field-level error messages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Testing Strategy
|
||||||
|
|
||||||
|
| Layer | Tool | Coverage Target |
|
||||||
|
|-------|------|----------------|
|
||||||
|
| Go unit tests | `go test` | 80%+ |
|
||||||
|
| Go integration tests | `go test -tags=integration` | Key flows |
|
||||||
|
| React unit tests | Vitest + React Testing Library | 70%+ |
|
||||||
|
| React E2E tests | Playwright | Critical flows |
|
||||||
|
| Manual testing | All platforms | Every sprint |
|
||||||
|
|
||||||
|
### Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Go backend tests
|
||||||
|
cd app/backend && go test ./...
|
||||||
|
|
||||||
|
# Frontend tests
|
||||||
|
cd app/frontend && npm test
|
||||||
|
|
||||||
|
# Full test suite
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Race condition check
|
||||||
|
go test -race ./app/backend/...
|
||||||
|
```
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
# Hostkeeper V2 — Build System
|
||||||
|
|
||||||
|
> **Status**: V2 Planning Complete
|
||||||
|
> **Last Updated**: 2026-06-29
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Build Overview
|
||||||
|
|
||||||
|
Hostkeeper V2 produces **4 platform outputs** from a single codebase:
|
||||||
|
|
||||||
|
| Platform | Output | Wrapper | Backend |
|
||||||
|
|----------|--------|---------|---------|
|
||||||
|
| macOS (ARM64) | `.dmg` | Electron | Go binary (embedded) |
|
||||||
|
| macOS (x64) | `.dmg` | Electron | Go binary (embedded) |
|
||||||
|
| Windows (x64) | `.exe` installer | Electron | Go binary (embedded) |
|
||||||
|
| Linux (x64) | `.AppImage` | Electron | Go binary (embedded) |
|
||||||
|
| Android (ARM64) | `.aab` / `.apk` | WebView | Go library (.aar) |
|
||||||
|
| iOS (ARM64) | `.ipa` | WKWebView | Go framework (.xcframework) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Build Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ Go Build │ │ Frontend │ │ Electron │ │ Platform │
|
||||||
|
│ (backend) │ → │ Build │ → │ Package │ → │ Sign │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1: Go Backend Cross-Compile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS ARM64
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \
|
||||||
|
go build -ldflags="-s -w" -o dist/hostkeeper-server .
|
||||||
|
|
||||||
|
# macOS x64
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 \
|
||||||
|
go build -ldflags="-s -w" -o dist/hostkeeper-server .
|
||||||
|
|
||||||
|
# Windows x64
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
|
||||||
|
go build -ldflags="-s -w" -o dist/hostkeeper-server.exe .
|
||||||
|
|
||||||
|
# Linux x64
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -ldflags="-s -w" -o dist/hostkeeper-server .
|
||||||
|
|
||||||
|
# Linux ARM64
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
|
||||||
|
go build -ldflags="-s -w" -o dist/hostkeeper-server .
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- `CGO_ENABLED=0` — pure Go, no C dependencies
|
||||||
|
- `-ldflags="-s -w"` — strip debug info, reduce binary size
|
||||||
|
- Go binary size: ~15-20 MB (compressed)
|
||||||
|
|
||||||
|
### Step 2: Frontend Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app/frontend
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
# Output: app/frontend/dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vite config** (`app/frontend/vite.config.ts`):
|
||||||
|
```typescript
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: false,
|
||||||
|
minify: 'terser',
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
xterm: ['@xterm/xterm', '@xterm/addon-fit'],
|
||||||
|
react: ['react', 'react-dom'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:8080',
|
||||||
|
'/ws': {
|
||||||
|
target: 'ws://localhost:8080',
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Electron Package
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# app/electron/electron-builder.yml
|
||||||
|
appId: com.hostkeeper.app
|
||||||
|
productName: Hostkeeper
|
||||||
|
copyright: Copyright © 2026
|
||||||
|
|
||||||
|
directories:
|
||||||
|
output: ../../dist
|
||||||
|
|
||||||
|
files:
|
||||||
|
- "**/*"
|
||||||
|
- "!**/node_modules/*/{CHANGELOG.md,README.md,readme.md,LICENSE}"
|
||||||
|
|
||||||
|
extraResources:
|
||||||
|
- from: "../backend/hostkeeper-server"
|
||||||
|
to: "hostkeeper-server"
|
||||||
|
- from: "../frontend/dist"
|
||||||
|
to: "frontend"
|
||||||
|
|
||||||
|
mac:
|
||||||
|
category: public.app-category.developer-tools
|
||||||
|
icon: assets/icon.icns
|
||||||
|
target:
|
||||||
|
- dmg
|
||||||
|
- zip
|
||||||
|
hardenedRuntime: true
|
||||||
|
notarize: true
|
||||||
|
|
||||||
|
win:
|
||||||
|
icon: assets/icon.ico
|
||||||
|
target:
|
||||||
|
- nsis
|
||||||
|
certificateFile: env.WIN_CERTIFICATE_FILE
|
||||||
|
|
||||||
|
linux:
|
||||||
|
icon: assets/icon.png
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- deb
|
||||||
|
category: Development
|
||||||
|
|
||||||
|
nsis:
|
||||||
|
oneClick: false
|
||||||
|
allowToChangeInstallationDirectory: true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Mobile Build
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prerequisites:
|
||||||
|
# - Android SDK installed
|
||||||
|
# - Go 1.26+ with gomobile
|
||||||
|
# - Java 17+
|
||||||
|
|
||||||
|
# Install gomobile
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
gomobile init
|
||||||
|
|
||||||
|
# Build Go library
|
||||||
|
cd mobile/gomobile
|
||||||
|
gomobile bind -target=android -o=../android/app/libs/hostkeeper.aar \
|
||||||
|
./go/
|
||||||
|
|
||||||
|
# Build Android app
|
||||||
|
cd ../android
|
||||||
|
./gradlew assembleRelease
|
||||||
|
# Output: mobile/android/app/build/outputs/apk/release/app-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Prerequisites:
|
||||||
|
# - Xcode 15+
|
||||||
|
# - Go 1.26+ with gomobile
|
||||||
|
|
||||||
|
# Build Go framework
|
||||||
|
cd mobile/gomobile
|
||||||
|
gomobile bind -target=ios -o=../ios/Hostkeeper/Hostkeeper.xcframework \
|
||||||
|
./go/
|
||||||
|
|
||||||
|
# Build iOS app
|
||||||
|
cd ../ios
|
||||||
|
xcodebuild -project Hostkeeper.xcodeproj \
|
||||||
|
-scheme Hostkeeper \
|
||||||
|
-sdk iphoneos \
|
||||||
|
-configuration Release
|
||||||
|
# Output: mobile/ios/build/Release-iphoneos/Hostkeeper.app
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Build Scripts
|
||||||
|
|
||||||
|
### `scripts/build.sh` (Full Build)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
VERSION=${1:-"dev"}
|
||||||
|
PLATFORM=${2:-"all"}
|
||||||
|
|
||||||
|
echo "=== Building Hostkeeper V2 v${VERSION} ==="
|
||||||
|
|
||||||
|
# Step 1: Build Go backend
|
||||||
|
echo "Step 1: Building Go backend..."
|
||||||
|
cd app/backend
|
||||||
|
|
||||||
|
case $PLATFORM in
|
||||||
|
macos|all)
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=${VERSION}" -o ../electron/hostkeeper-server .
|
||||||
|
;;
|
||||||
|
windows|all)
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.version=${VERSION}" -o ../electron/hostkeeper-server.exe .
|
||||||
|
;;
|
||||||
|
linux|all)
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${VERSION}" -o ../electron/hostkeeper-server .
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Step 2: Build frontend
|
||||||
|
echo "Step 2: Building frontend..."
|
||||||
|
cd app/frontend
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Step 3: Package Electron
|
||||||
|
echo "Step 3: Packaging Electron..."
|
||||||
|
cd app/electron
|
||||||
|
|
||||||
|
case $PLATFORM in
|
||||||
|
macos)
|
||||||
|
npx electron-builder --mac --arm64
|
||||||
|
;;
|
||||||
|
windows)
|
||||||
|
npx electron-builder --win --x64
|
||||||
|
;;
|
||||||
|
linux)
|
||||||
|
npx electron-builder --linux --x64
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
npx electron-builder --mac --win --linux
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Step 4: Generate checksums
|
||||||
|
echo "Step 4: Generating checksums..."
|
||||||
|
cd dist
|
||||||
|
shasum -a 256 *.dmg *.exe *.AppImage 2>/dev/null > checksums.txt
|
||||||
|
|
||||||
|
echo "=== Build complete ==="
|
||||||
|
echo "Output: dist/"
|
||||||
|
ls -la dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
### `scripts/build-mobile.sh`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PLATFORM=${1:-"android"}
|
||||||
|
|
||||||
|
echo "=== Building Hostkeeper Mobile (${PLATFORM}) ==="
|
||||||
|
|
||||||
|
case $PLATFORM in
|
||||||
|
android)
|
||||||
|
cd mobile/gomobile
|
||||||
|
gomobile bind -target=android -o=../android/app/libs/hostkeeper.aar ./go/
|
||||||
|
cd ../android
|
||||||
|
./gradlew assembleRelease
|
||||||
|
echo "APK: mobile/android/app/build/outputs/apk/release/"
|
||||||
|
;;
|
||||||
|
ios)
|
||||||
|
cd mobile/gomobile
|
||||||
|
gomobile bind -target=ios -o=../ios/Hostkeeper/Hostkeeper.xcframework ./go/
|
||||||
|
cd ../ios
|
||||||
|
xcodebuild -project Hostkeeper.xcodeproj -scheme Hostkeeper -sdk iphoneos -configuration Release
|
||||||
|
echo "IPA: mobile/ios/build/Release-iphoneos/"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "=== Mobile build complete ==="
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Development Workflow
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Go backend
|
||||||
|
cd app/backend
|
||||||
|
go run .
|
||||||
|
|
||||||
|
# Terminal 2: Frontend (with hot reload)
|
||||||
|
cd app/frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Open browser: http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Electron
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Go backend
|
||||||
|
cd app/backend
|
||||||
|
go run .
|
||||||
|
|
||||||
|
# Terminal 2: Frontend
|
||||||
|
cd app/frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Terminal 3: Electron
|
||||||
|
cd app/electron
|
||||||
|
npm run dev
|
||||||
|
# Electron opens with hot reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code Launch Config
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Go Backend",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/app/backend",
|
||||||
|
"cwd": "${workspaceFolder}/app/backend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Electron",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/app/electron/node_modules/.bin/electron",
|
||||||
|
"args": ["."],
|
||||||
|
"cwd": "${workspaceFolder}/app/electron"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. CI/CD (GitHub Actions)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/build.yml
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
- run: go test -race ./app/backend/...
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
- run: cd app/frontend && npm ci && npm test
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
needs: test
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
- run: ./scripts/build.sh ${{ github.ref_name }} macos
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: hostkeeper-macos
|
||||||
|
path: dist/*.dmg
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
needs: test
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
- run: ./scripts/build.sh ${{ github.ref_name }} windows
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: hostkeeper-windows
|
||||||
|
path: dist/*.exe
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
- run: ./scripts/build.sh ${{ github.ref_name }} linux
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: hostkeeper-linux
|
||||||
|
path: dist/*.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Binary Size Optimization
|
||||||
|
|
||||||
|
| Technique | Impact |
|
||||||
|
|-----------|--------|
|
||||||
|
| `-ldflags="-s -w"` | -20% Go binary size |
|
||||||
|
| UPX compression (optional) | -60% Go binary size |
|
||||||
|
| Vite tree shaking | -40% JS bundle size |
|
||||||
|
| Code splitting (lazy load) | -30% initial load |
|
||||||
|
| Image optimization | -50% icon sizes |
|
||||||
|
| Terser minification | -30% JS size |
|
||||||
|
| CSS purging (Tailwind) | -80% CSS size |
|
||||||
|
|
||||||
|
**Target sizes**:
|
||||||
|
- Go backend binary: ~15 MB (5 MB compressed)
|
||||||
|
- Frontend dist: ~2 MB (500 KB compressed)
|
||||||
|
- Electron package: ~200 MB total
|
||||||
|
- Mobile APK: ~50 MB
|
||||||
|
- Mobile IPA: ~60 MB
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Version Management
|
||||||
|
|
||||||
|
### Version Format
|
||||||
|
|
||||||
|
```
|
||||||
|
vMAJOR.MINOR.PATCH
|
||||||
|
```
|
||||||
|
|
||||||
|
- **MAJOR**: Breaking changes (data format, API)
|
||||||
|
- **MINOR**: New features
|
||||||
|
- **PATCH**: Bug fixes
|
||||||
|
|
||||||
|
### Version Injection
|
||||||
|
|
||||||
|
```go
|
||||||
|
// In main.go
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Printf("Hostkeeper v%s\n", version)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with version
|
||||||
|
go build -ldflags="-X main.version=v2.0.0" -o hostkeeper-server .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changelog Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [v2.1.0] - 2026-07-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Port forwarding support (local, remote, dynamic)
|
||||||
|
- Import from ~/.ssh/config
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved terminal performance with WebGL renderer
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed SFTP upload progress not updating
|
||||||
|
- Fixed vault auto-lock not triggering on sleep
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Code Signing
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires Apple Developer account + certificates
|
||||||
|
export CSC_LINK="path/to/certificate.p12"
|
||||||
|
export CSC_KEY_PASSWORD="certificate-password"
|
||||||
|
|
||||||
|
# electron-builder handles signing + notarization
|
||||||
|
npx electron-builder --mac --publish always
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Requires code signing certificate
|
||||||
|
export WIN_CERTIFICATE_FILE="path/to/certificate.pfx"
|
||||||
|
export WIN_CERTIFICATE_PASSWORD="certificate-password"
|
||||||
|
|
||||||
|
npx electron-builder --win --publish always
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
No code signing required. AppImage is self-contained.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Release Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Update version
|
||||||
|
npm version minor # or major, patch
|
||||||
|
|
||||||
|
# 2. Update CHANGELOG.md
|
||||||
|
|
||||||
|
# 3. Commit
|
||||||
|
git add .
|
||||||
|
git commit -m "chore: release v2.1.0"
|
||||||
|
|
||||||
|
# 4. Tag
|
||||||
|
git tag -a v2.1.0 -m "Release v2.1.0"
|
||||||
|
|
||||||
|
# 5. Push
|
||||||
|
git push origin main --tags
|
||||||
|
|
||||||
|
# 6. GitHub Actions builds + publishes artifacts
|
||||||
|
|
||||||
|
# 7. Create GitHub Release
|
||||||
|
gh release create v2.1.0 \
|
||||||
|
--title "Hostkeeper v2.1.0" \
|
||||||
|
--notes-file CHANGELOG.md \
|
||||||
|
dist/*.dmg dist/*.exe dist/*.AppImage
|
||||||
|
```
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
> **Purpose**: Enable seamless continuation of development by any agent/LLM across sessions.
|
> **Purpose**: Enable seamless continuation of development by any agent/LLM across sessions.
|
||||||
> **How to use**: Read this file first. Find the first unchecked `[ ]` item. That's where you continue.
|
> **How to use**: Read this file first. Find the first unchecked `[ ]` item. That's where you continue.
|
||||||
> **Last Updated**: 2026-07-07
|
> **Last Updated**: 2026-06-29
|
||||||
> **Status**: Documentation complete, Sprint 0 not started.
|
> **Status**: Planning complete, implementation not started.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,33 +14,28 @@ Hostkeeper V2 is a Termius-like cross-platform SSH/SFTP management GUI applicati
|
|||||||
| Item | Value |
|
| Item | Value |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| **Goal** | Build a free, open-source Termius alternative with full feature parity |
|
| **Goal** | Build a free, open-source Termius alternative with full feature parity |
|
||||||
| **Desktop** | GoFiber backend + HTMX + xterm.js + Alpine.js |
|
| **Desktop** | GoFiber backend + React/xterm.js UI + Electron wrapper |
|
||||||
| **Mobile** | Go mobile library + WebView wrapper (Android/iOS) |
|
| **Mobile** | Go mobile library + WebView wrapper (Android/iOS) |
|
||||||
| **Backend language** | Go 1.26 |
|
| **Backend language** | Go 1.26 |
|
||||||
| **HTTP framework** | GoFiber v2 (fasthttp-based) |
|
| **HTTP framework** | GoFiber v2 (fasthttp-based) |
|
||||||
| **Frontend** | HTMX + Alpine.js + xterm.js (minimal JS, server-rendered HTML) |
|
| **Frontend** | React 19, TypeScript, xterm.js, Zustand, Vite |
|
||||||
| **Styling** | TailwindCSS v4 + custom CSS (Lumina System) |
|
| **Desktop wrapper** | Electron (electron-builder) |
|
||||||
| **Desktop wrapper** | Electron (opsional — bisa lewat browser saja) |
|
|
||||||
| **Mobile wrapper** | gomobile + WebView |
|
| **Mobile wrapper** | gomobile + WebView |
|
||||||
| **V1 (existing)** | CLI/TUI SSH/SFTP manager — 105 tests, all passing. Frozen. |
|
| **V1 (existing)** | CLI/TUI SSH/SFTP manager — 105 tests, all passing. Frozen. |
|
||||||
|
|
||||||
**Keputusan arsitektur**: Migrasi dari React/SPA ke HTMX/server-rendered HTML.
|
|
||||||
Lihat [ARCHITECTURE_HTMX.md](ARCHITECTURE_HTMX.md) untuk detail.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Current Status Summary
|
## 2. Current Status Summary
|
||||||
|
|
||||||
| Item | Status |
|
| Item | Status |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| Planning documents (HTMX) | DONE |
|
| Planning documents | DONE |
|
||||||
| Sprint 0 — Setup GoFiber + HTMX | NOT STARTED |
|
| Sprint 0 — Scaffolding | NOT STARTED |
|
||||||
| Sprint 1 — Dashboard + Host List | NOT STARTED |
|
| Sprint 1 — Foundation | NOT STARTED |
|
||||||
| Sprint 2 — Snippets + Keychain | NOT STARTED |
|
| Sprint 2 — Core Features | NOT STARTED |
|
||||||
| Sprint 3 — Settings + Brief | NOT STARTED |
|
| Sprint 3 — SFTP + Polish | NOT STARTED |
|
||||||
| Sprint 4 — SFTP Dual-Pane | NOT STARTED |
|
| Sprint 4 — Advanced | NOT STARTED |
|
||||||
| Sprint 5 — Terminal + xterm.js | NOT STARTED |
|
| Sprint 5 — Platform Polish | NOT STARTED |
|
||||||
| Sprint 6 — Storage Integration | NOT STARTED |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -295,15 +290,15 @@ Lihat [ARCHITECTURE_HTMX.md](ARCHITECTURE_HTMX.md) untuk detail.
|
|||||||
|
|
||||||
### For a new AI agent:
|
### For a new AI agent:
|
||||||
|
|
||||||
1. **Read this file** (`docs/PROGRESS.md`) — find the first `[ ]` item
|
1. **Read this file** (`docs/v2/PROGRESS.md`) — find the first `[ ]` item
|
||||||
2. **Read the relevant spec doc** — for the current task:
|
2. **Read the relevant spec doc** — for the current task:
|
||||||
- REST API endpoints -> `docs/API.md`
|
- REST API endpoints -> `docs/v2/API.md`
|
||||||
- UI components -> `docs/UI_COMPONENTS.md`
|
- UI components -> `docs/v2/UI_COMPONENTS.md`
|
||||||
- Data models -> `docs/DATA_MODELS.md`
|
- Data models -> `docs/v2/DATA_MODELS.md`
|
||||||
- Visual reference -> `docs/UI_TERMIUS_REFERENCE.md`
|
- Visual reference -> `docs/v2/UI_TERMIUS_REFERENCE.md`
|
||||||
- Performance notes -> `docs/PERFORMANCE.md`
|
- Performance notes -> `docs/v2/PERFORMANCE.md`
|
||||||
- Build pipeline -> `docs/BUILD_SYSTEM.md`
|
- Build pipeline -> `docs/v2/BUILD_SYSTEM.md`
|
||||||
3. **Read `docs/ARCHITECTURE.md`** — understand the system
|
3. **Read `docs/v2/ARCHITECTURE.md`** — understand the system
|
||||||
4. **Implement** in `app/backend/` (Go) or `app/frontend/` (React)
|
4. **Implement** in `app/backend/` (Go) or `app/frontend/` (React)
|
||||||
5. **Test** — run `go test ./app/backend/...` and `npm test` in `app/frontend/`
|
5. **Test** — run `go test ./app/backend/...` and `npm test` in `app/frontend/`
|
||||||
6. **Update this file** — mark `[x]` the completed task, update "Last Session Notes"
|
6. **Update this file** — mark `[x]` the completed task, update "Last Session Notes"
|
||||||
@@ -321,15 +316,15 @@ Lihat [ARCHITECTURE_HTMX.md](ARCHITECTURE_HTMX.md) untuk detail.
|
|||||||
|
|
||||||
| Document | When to read |
|
| Document | When to read |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| `docs/PROGRESS.md` | Always (first) — know where to continue |
|
| `docs/v2/PROGRESS.md` | Always (first) — know where to continue |
|
||||||
| `docs/ARCHITECTURE.md` | Understanding the system |
|
| `docs/v2/ARCHITECTURE.md` | Understanding the system |
|
||||||
| `docs/API.md` | Implementing REST endpoints or WebSocket handlers |
|
| `docs/v2/API.md` | Implementing REST endpoints or WebSocket handlers |
|
||||||
| `docs/DATA_MODELS.md` | Creating/modifying Go structs or JSON schemas |
|
| `docs/v2/DATA_MODELS.md` | Creating/modifying Go structs or JSON schemas |
|
||||||
| `docs/UI_COMPONENTS.md` | Building React components |
|
| `docs/v2/UI_COMPONENTS.md` | Building React components |
|
||||||
| `docs/UI_TERMIUS_REFERENCE.md` | Designing UI layout, colors, typography |
|
| `docs/v2/UI_TERMIUS_REFERENCE.md` | Designing UI layout, colors, typography |
|
||||||
| `docs/SPRINT_PLAN.md` | Detailed file-by-file task instructions |
|
| `docs/v2/SPRINT_PLAN.md` | Detailed file-by-file task instructions |
|
||||||
| `docs/BUILD_SYSTEM.md` | Setting up build pipeline |
|
| `docs/v2/BUILD_SYSTEM.md` | Setting up build pipeline |
|
||||||
| `docs/PERFORMANCE.md` | Tuning for speed and stability |
|
| `docs/v2/PERFORMANCE.md` | Tuning for speed and stability |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
# Hostkeeper V2 — Sprint Plan (File-by-File Task Breakdown)
|
||||||
|
|
||||||
|
> **Status**: V2 Planning Complete
|
||||||
|
> **Last Updated**: 2026-06-29
|
||||||
|
> **Framework**: GoFiber v2 (backend), React/TypeScript (frontend), Electron (desktop)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint 0 — Project Scaffolding
|
||||||
|
|
||||||
|
**Goal**: Empty Electron app with GoFiber backend serving "Hello World".
|
||||||
|
|
||||||
|
### Day 0: Setup (1 day)
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 0.1 | Go module for backend | `app/backend/go.mod` | `go mod init` runs clean |
|
||||||
|
| 0.2 | Install GoFiber + deps | `app/backend/go.mod` | `go get github.com/gofiber/fiber/v2` |
|
||||||
|
| 0.3 | Minimal fiber server | `app/backend/main.go` | Server starts on random port |
|
||||||
|
| 0.4 | Health endpoint | `app/backend/main.go` | `curl localhost:PORT/api/health` returns JSON |
|
||||||
|
| 0.5 | Middleware setup | `app/backend/middleware/middleware.go` | CORS, logger, recover, compress registered |
|
||||||
|
| 0.6 | Vite + React scaffold | `app/frontend/package.json`, `app/frontend/src/App.tsx` | `npm run dev` shows React app |
|
||||||
|
| 0.7 | Electron shell | `app/electron/main.ts`, `app/electron/preload.ts`, `app/electron/package.json` | `npx electron .` opens window |
|
||||||
|
| 0.8 | Electron spawns Go | `app/electron/main.ts` | Go process starts, API responds |
|
||||||
|
| 0.9 | Electron loads frontend | `app/electron/main.ts` | Window shows React app |
|
||||||
|
| 0.10 | Bridge placeholder dirs | `app/backend/bridge/`, `app/backend/ws/` | Directories exist |
|
||||||
|
| 0.11 | Build script | `scripts/build.sh` | Script runs without error |
|
||||||
|
| 0.12 | End-to-end verify | — | Electron window shows "Hostkeeper V2" title |
|
||||||
|
|
||||||
|
#### Detailed Files
|
||||||
|
|
||||||
|
**`app/backend/go.mod`**:
|
||||||
|
```go
|
||||||
|
module git.tukangketik.id/swanadiva/hostkeeper/app/backend
|
||||||
|
|
||||||
|
go 1.26
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
|
github.com/gofiber/contrib/websocket v1.3.3
|
||||||
|
github.com/fasthttp/websocket v1.5.12
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**`app/backend/main.go`**:
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
port := rand.Intn(60000) + 10000 // random port 10000-70000
|
||||||
|
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 30 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.Use(recover.New())
|
||||||
|
app.Use(logger.New())
|
||||||
|
app.Use(compress.New(compress.Config{
|
||||||
|
Level: compress.LevelBestSpeed,
|
||||||
|
}))
|
||||||
|
app.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: "*",
|
||||||
|
AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
||||||
|
AllowHeaders: "Origin,Content-Type,Authorization",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.Get("/api/health", func(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": "ok",
|
||||||
|
"version": "2.0.0",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
go func() {
|
||||||
|
fmt.Printf("HOSTKEEPER_PORT=%d\n", port)
|
||||||
|
log.Fatal(app.Listen(fmt.Sprintf(":%d", port)))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
app.Shutdown()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`app/electron/main.ts`**:
|
||||||
|
```typescript
|
||||||
|
import { app, BrowserWindow } from 'electron';
|
||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
let mainWindow: BrowserWindow;
|
||||||
|
let goProcess: ChildProcess;
|
||||||
|
|
||||||
|
function startGoBackend() {
|
||||||
|
const goBinary = path.join(__dirname, '../../backend/hostkeeper-server');
|
||||||
|
goProcess = spawn(goBinary, [], { stdio: 'pipe' });
|
||||||
|
|
||||||
|
goProcess.stdout?.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
const match = output.match(/HOSTKEEPER_PORT=(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
const port = match[1];
|
||||||
|
mainWindow.loadURL(`http://localhost:${port}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
title: 'Hostkeeper V2',
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load from Go backend (or dev server in development)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
mainWindow.loadURL('http://localhost:5173');
|
||||||
|
} else {
|
||||||
|
startGoBackend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(createWindow);
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
goProcess?.kill();
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**`scripts/build.sh`**:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Building Hostkeeper V2 ==="
|
||||||
|
|
||||||
|
# Build Go backend
|
||||||
|
echo "Building Go backend..."
|
||||||
|
cd app/backend
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o ../electron/hostkeeper-server .
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
echo "Building frontend..."
|
||||||
|
cd app/frontend
|
||||||
|
npm run build
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
# Package Electron
|
||||||
|
echo "Packaging Electron..."
|
||||||
|
cd app/electron
|
||||||
|
npx electron-builder --mac --arm64
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
echo "=== Build complete ==="
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint 1 — Foundation (Day 1-5)
|
||||||
|
|
||||||
|
**Goal**: Full CRUD for hosts/keys/snippets + web UI with host list.
|
||||||
|
|
||||||
|
### Day 1-2: Data API Layer
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 1.1 | Host CRUD handlers | `app/backend/bridge/hosts.go` | All 6 endpoints work via curl |
|
||||||
|
| 1.2 | Key CRUD handlers | `app/backend/bridge/keys.go` | All 4 endpoints work via curl |
|
||||||
|
| 1.3 | Snippet CRUD handlers | `app/backend/bridge/snippets.go` | All 4 endpoints work via curl |
|
||||||
|
| 1.4 | Storage integration | Import `pkg/storage` | Read/write actual JSON files |
|
||||||
|
| 1.5 | Error responses | `app/backend/bridge/errors.go` | Consistent error format |
|
||||||
|
| 1.6 | Route registration | `app/backend/main.go` | All routes registered |
|
||||||
|
|
||||||
|
**Example handler** (`bridge/hosts.go`):
|
||||||
|
```go
|
||||||
|
func ListHosts(c *fiber.Ctx) error {
|
||||||
|
hosts, err := storage.ListHosts(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"items": hosts, "total": len(hosts)})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Day 3: Groups + Config API
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 1.7 | Group CRUD handlers | `app/backend/bridge/groups.go` | Tree structure returned |
|
||||||
|
| 1.8 | Config handlers | `app/backend/bridge/config.go` | Get/set config works |
|
||||||
|
| 1.9 | Vault handlers | `app/backend/bridge/vault.go` | Lock/unlock/unlock status |
|
||||||
|
| 1.10 | Group tree builder | `app/backend/bridge/groups.go` | Recursive tree populated |
|
||||||
|
|
||||||
|
### Day 4: Web UI Scaffold
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 1.11 | React Router | `app/frontend/src/router.tsx` | Navigate between screens |
|
||||||
|
| 1.12 | Layout component | `app/frontend/src/components/Layout.tsx` | Sidebar + tab bar + content |
|
||||||
|
| 1.13 | Sidebar component | `app/frontend/src/components/Sidebar.tsx` | Group tree + search |
|
||||||
|
| 1.14 | Theme system | `app/frontend/src/styles/themes.css` | CSS variables for dark/light |
|
||||||
|
| 1.15 | API client | `app/frontend/src/api/client.ts` | Fetch wrapper with errors |
|
||||||
|
| 1.16 | Zustand stores | `app/frontend/src/stores/*.ts` | Host, tab, vault stores |
|
||||||
|
| 1.17 | Keyboard shortcuts | `app/frontend/src/hooks/useKeyboard.ts` | Cmd+N, Cmd+K work |
|
||||||
|
|
||||||
|
### Day 5: Host List Screen
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 1.18 | HostList screen | `app/frontend/src/screens/HostListScreen.tsx` | Hosts displayed |
|
||||||
|
| 1.19 | HostCard component | `app/frontend/src/components/HostCard.tsx` | Host info shown |
|
||||||
|
| 1.20 | GroupTree component | `app/frontend/src/components/GroupTree.tsx` | Collapsible tree |
|
||||||
|
| 1.21 | SearchBar component | `app/frontend/src/components/SearchBar.tsx` | Filter works |
|
||||||
|
| 1.22 | EmptyState component | `app/frontend/src/components/EmptyState.tsx` | Shown when no hosts |
|
||||||
|
| 1.23 | Context menu | `app/frontend/src/components/ContextMenu.tsx` | Right-click menu |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint 2 — Core Features (Day 6-10)
|
||||||
|
|
||||||
|
**Goal**: Host form + SSH terminal (the critical feature).
|
||||||
|
|
||||||
|
### Day 6-7: Host Form + CRUD UI
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 2.1 | HostForm screen | `app/frontend/src/screens/HostFormScreen.tsx` | Add/edit host works |
|
||||||
|
| 2.2 | AuthSection component | `app/frontend/src/components/AuthSection.tsx` | Password/key selector |
|
||||||
|
| 2.3 | TagsInput component | `app/frontend/src/components/TagsInput.tsx` | Add/remove tags |
|
||||||
|
| 2.4 | GroupPicker component | `app/frontend/src/components/GroupPicker.tsx` | Tree picker |
|
||||||
|
| 2.5 | Form validation | `app/frontend/src/utils/validation.ts` | All field validation |
|
||||||
|
| 2.6 | Toast notifications | `app/frontend/src/components/Toast.tsx` | Success/error toasts |
|
||||||
|
|
||||||
|
### Day 8-10: SSH Terminal (MOST CRITICAL)
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 2.7 | WebSocket handler | `app/backend/ws/terminal.go` | WS connection accepted |
|
||||||
|
| 2.8 | SSH session management | `app/backend/ws/session.go` | Connect, stream, disconnect |
|
||||||
|
| 2.9 | Terminal resize | `app/backend/ws/terminal.go` | Resize messages handled |
|
||||||
|
| 2.10 | XTermWrapper component | `app/frontend/src/components/XTermWrapper.tsx` | Terminal renders |
|
||||||
|
| 2.11 | TerminalScreen | `app/frontend/src/screens/TerminalScreen.tsx` | Full terminal view |
|
||||||
|
| 2.12 | TabBar component | `app/frontend/src/components/TabBar.tsx` | Multi-tab support |
|
||||||
|
| 2.13 | Tab management | `app/frontend/src/stores/tabStore.ts` | Add/close/switch tabs |
|
||||||
|
| 2.14 | SnippetPanel | `app/frontend/src/components/SnippetPanel.tsx` | Slide-in snippet list |
|
||||||
|
| 2.15 | Connection overlay | `app/frontend/src/components/ConnectionOverlay.tsx` | Status + reconnect |
|
||||||
|
| 2.16 | Terminal toolbar | `app/frontend/src/components/TerminalToolbar.tsx` | Host info + actions |
|
||||||
|
|
||||||
|
**WebSocket handler** (`ws/terminal.go`):
|
||||||
|
```go
|
||||||
|
package ws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/contrib/websocket"
|
||||||
|
"git.tukangketik.id/swanadiva/hostkeeper/pkg/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleTerminal(c *websocket.Conn) {
|
||||||
|
hostID := c.Query("host_id")
|
||||||
|
cols := c.QueryInt("cols", 80)
|
||||||
|
rows := c.QueryInt("rows", 24)
|
||||||
|
|
||||||
|
// Get host from storage
|
||||||
|
host, err := storage.GetHost(c.Context(), hostID)
|
||||||
|
if err != nil {
|
||||||
|
c.WriteJSON(fiber.Map{"type": "error", "message": "Host not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SSH client
|
||||||
|
client := ssh.NewClient(host, 30*time.Second)
|
||||||
|
if err := client.Connect(c.Context()); err != nil {
|
||||||
|
c.WriteJSON(fiber.Map{"type": "error", "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Create session + PTY
|
||||||
|
session, err := client.GetClient().NewSession()
|
||||||
|
if err != nil {
|
||||||
|
c.WriteJSON(fiber.Map{"type": "error", "message": "Session failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer session.Close()
|
||||||
|
|
||||||
|
modes := ssh.TerminalModes{ssh.ECHO: 1}
|
||||||
|
session.RequestPty("xterm-256color", rows, cols, modes)
|
||||||
|
|
||||||
|
stdin, _ := session.StdinPipe()
|
||||||
|
stdout, _ := session.StdoutPipe()
|
||||||
|
|
||||||
|
if err := session.Shell(); err != nil {
|
||||||
|
c.WriteJSON(fiber.Map{"type": "error", "message": "Shell failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.WriteJSON(fiber.Map{"type": "status", "state": "connected"})
|
||||||
|
|
||||||
|
// Goroutine: SSH stdout -> WebSocket
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
n, err := stdout.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := c.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Main loop: WebSocket -> SSH stdin
|
||||||
|
for {
|
||||||
|
_, msg, err := c.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for resize message
|
||||||
|
if len(msg) > 0 && msg[0] == '{' {
|
||||||
|
var ctrl struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Cols int `json:"cols"`
|
||||||
|
Rows int `json:"rows"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(msg, &ctrl) == nil && ctrl.Type == "resize" {
|
||||||
|
session.WindowChange(ctrl.Rows, ctrl.Cols)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular input
|
||||||
|
stdin.Write(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint 3 — SFTP + Polish (Day 11-15)
|
||||||
|
|
||||||
|
**Goal**: SFTP browser + security + settings.
|
||||||
|
|
||||||
|
### Day 11-12: SFTP Browser
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 3.1 | SFTP REST handlers | `app/backend/bridge/sftp.go` | All 7 endpoints work |
|
||||||
|
| 3.2 | SFTPScreen | `app/frontend/src/screens/SFTPScreen.tsx` | Dual pane visible |
|
||||||
|
| 3.3 | FileList component | `app/frontend/src/components/FileList.tsx` | Files listed with columns |
|
||||||
|
| 3.4 | LocalPane component | `app/frontend/src/components/LocalPane.tsx` | Local files |
|
||||||
|
| 3.5 | RemotePane component | `app/frontend/src/components/RemotePane.tsx` | Remote files |
|
||||||
|
| 3.6 | TransferQueue | `app/frontend/src/components/TransferQueue.tsx` | Transfer progress |
|
||||||
|
| 3.7 | SFTPToolbar | `app/frontend/src/components/SFTPToolbar.tsx` | Upload/download buttons |
|
||||||
|
| 3.8 | Context menu | `app/frontend/src/components/SFTPContextMenu.tsx` | File operations |
|
||||||
|
|
||||||
|
### Day 13: Security + Vault
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 3.9 | VaultScreen | `app/frontend/src/screens/VaultScreen.tsx` | Password prompt |
|
||||||
|
| 3.10 | KnownHostDialog | `app/frontend/src/components/KnownHostDialog.tsx` | Fingerprint verify |
|
||||||
|
| 3.11 | PasswordPrompt | `app/frontend/src/components/PasswordPrompt.tsx` | Reusable prompt |
|
||||||
|
| 3.12 | Auto-lock logic | `app/frontend/src/hooks/useAutoLock.ts` | Locks after inactivity |
|
||||||
|
|
||||||
|
### Day 14: Settings Screen
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 3.13 | SettingsScreen | `app/frontend/src/screens/SettingsScreen.tsx` | Tabbed settings |
|
||||||
|
| 3.14 | AppearanceSettings | `app/frontend/src/components/settings/Appearance.tsx` | Theme, font |
|
||||||
|
| 3.15 | TerminalSettings | `app/frontend/src/components/settings/Terminal.tsx` | Scrollback, cursor |
|
||||||
|
| 3.16 | ConnectionSettings | `app/frontend/src/components/settings/Connection.tsx` | Timeout, keepalive |
|
||||||
|
| 3.17 | VaultSettings | `app/frontend/src/components/settings/Vault.tsx` | Password, auto-lock |
|
||||||
|
| 3.18 | GeneralSettings | `app/frontend/src/components/settings/General.tsx` | About, data dir |
|
||||||
|
|
||||||
|
### Day 15: Mobile Setup
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 3.19 | gomobile build script | `mobile/gomobile/build.sh` | Compiles to .aar/.xcframework |
|
||||||
|
| 3.20 | Android WebView app | `mobile/android/` | App runs on emulator |
|
||||||
|
| 3.21 | iOS WebView app | `mobile/ios/` | App runs on simulator |
|
||||||
|
| 3.22 | Mobile responsive CSS | `app/frontend/src/styles/mobile.css` | Touch-friendly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint 4 — Advanced Features (Day 16-20)
|
||||||
|
|
||||||
|
**Goal**: Port forwarding + import/export + jump hosts.
|
||||||
|
|
||||||
|
### Day 16-17: Port Forwarding
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 4.1 | Forward model + storage | `app/backend/bridge/portforward.go` | CRUD works |
|
||||||
|
| 4.2 | Local forwarding | `app/backend/ws/portforward.go` | Local tunnel works |
|
||||||
|
| 4.3 | Remote forwarding | `app/backend/ws/portforward.go` | Remote tunnel works |
|
||||||
|
| 4.4 | Dynamic forwarding | `app/backend/ws/portforward.go` | SOCKS5 proxy works |
|
||||||
|
| 4.5 | PortForwardScreen | `app/frontend/src/screens/PortForwardScreen.tsx` | List + add form |
|
||||||
|
| 4.6 | ForwardCard component | `app/frontend/src/components/ForwardCard.tsx` | Status + controls |
|
||||||
|
|
||||||
|
### Day 18: Import/Export
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 4.7 | SSH config parser | `app/backend/bridge/import.go` | Parses ~/.ssh/config |
|
||||||
|
| 4.8 | CSV import | `app/backend/bridge/import.go` | Parses CSV |
|
||||||
|
| 4.9 | Export handler | `app/backend/bridge/export.go` | Encrypted JSON export |
|
||||||
|
| 4.10 | ImportScreen | `app/frontend/src/screens/ImportScreen.tsx` | Drag & drop UI |
|
||||||
|
|
||||||
|
### Day 19-20: Jump Hosts + Proxy
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 4.11 | Jump host chain | `app/backend/ws/jump.go` | Multi-hop SSH works |
|
||||||
|
| 4.12 | SOCKS5 proxy | `app/backend/ws/proxy.go` | Proxy before SSH |
|
||||||
|
| 4.13 | AdvancedHostForm | `app/frontend/src/components/AdvancedHostForm.tsx` | Jump host selector |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint 5 — Platform Polish (Day 21-25)
|
||||||
|
|
||||||
|
**Goal**: Desktop polish + performance + mobile.
|
||||||
|
|
||||||
|
### Day 21-22: Desktop Enhancements
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 5.1 | Native menu bar | `app/electron/menu.ts` | File/Edit/View/Help |
|
||||||
|
| 5.2 | System tray | `app/electron/tray.ts` | Tray icon + menu |
|
||||||
|
| 5.3 | Global shortcuts | `app/electron/shortcuts.ts` | Cmd+`, Cmd+N |
|
||||||
|
| 5.4 | Window state | `app/electron/state.ts` | Position/size saved |
|
||||||
|
| 5.5 | Auto-update | `app/electron/updater.ts` | Update prompt |
|
||||||
|
| 5.6 | OS Keychain | `app/electron/keychain.ts` | Store vault key |
|
||||||
|
|
||||||
|
### Day 23: Performance Optimization
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 5.7 | SSH connection pool | `app/backend/ws/pool.go` | Reuse connections |
|
||||||
|
| 5.8 | Buffer pools | `app/backend/ws/buffers.go` | sync.Pool for buffers |
|
||||||
|
| 5.9 | Virtual scrolling | `app/frontend/src/components/VirtualList.tsx` | 1000+ hosts smooth |
|
||||||
|
| 5.10 | Code splitting | `app/frontend/src/router.tsx` | Lazy load screens |
|
||||||
|
| 5.11 | React.memo | Various components | No unnecessary re-renders |
|
||||||
|
| 5.12 | WebGL renderer | `app/frontend/src/components/XTermWrapper.tsx` | Fast terminal render |
|
||||||
|
|
||||||
|
### Day 24-25: Mobile Polish
|
||||||
|
|
||||||
|
| # | Task | Files to Create | Verify |
|
||||||
|
|---|------|----------------|--------|
|
||||||
|
| 5.13 | Android gomobile | `mobile/android/` | .aar compiled |
|
||||||
|
| 5.14 | iOS gomobile | `mobile/ios/` | .xcframework compiled |
|
||||||
|
| 5.15 | Mobile keyboard | `app/frontend/src/components/MobileKeyboard.tsx` | Terminal toolbar |
|
||||||
|
| 5.16 | Gesture navigation | `app/frontend/src/hooks/useGesture.ts` | Swipe to switch tabs |
|
||||||
|
| 5.17 | Landscape handling | `app/frontend/src/styles/mobile.css` | Responsive layout |
|
||||||
|
| 5.18 | Bluetooth keyboard | `app/frontend/src/hooks/useKeyboard.ts` | External keyboard |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
Sprint 0 (Scaffolding)
|
||||||
|
│
|
||||||
|
├── Sprint 1 (Foundation)
|
||||||
|
│ │
|
||||||
|
│ ├── Sprint 2 (Core Features)
|
||||||
|
│ │ │
|
||||||
|
│ │ ├── Sprint 3 (SFTP + Polish)
|
||||||
|
│ │ │ │
|
||||||
|
│ │ │ └── Sprint 5 (Platform Polish)
|
||||||
|
│ │ │
|
||||||
|
│ │ └── Sprint 4 (Advanced Features)
|
||||||
|
│ │ │
|
||||||
|
│ │ └── Sprint 5 (Platform Polish)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical path**: Sprint 0 → Sprint 1 → Sprint 2 → Sprint 5
|
||||||
|
|
||||||
|
**Parallel tracks**:
|
||||||
|
- Sprint 3 and Sprint 4 can be done in parallel
|
||||||
|
- Sprint 5 can start after Sprint 2 (desktop polish), Sprint 3 (SFTP mobile), Sprint 4 (advanced mobile)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Creation Summary
|
||||||
|
|
||||||
|
| Sprint | New Files | Directories |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| Sprint 0 | 12 | 6 |
|
||||||
|
| Sprint 1 | 23 | 2 |
|
||||||
|
| Sprint 2 | 16 | 2 |
|
||||||
|
| Sprint 3 | 22 | 4 |
|
||||||
|
| Sprint 4 | 13 | 1 |
|
||||||
|
| Sprint 5 | 18 | 2 |
|
||||||
|
| **Total** | **104** | **17** |
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
# Hostkeeper V2 — Timeline & Milestones
|
||||||
|
|
||||||
|
> **Status**: V2 Planning Complete
|
||||||
|
> **Last Updated**: 2026-06-29
|
||||||
|
> **Start Date**: 2026-06-30 (estimated)
|
||||||
|
> **Target MVP**: 2026-08-07 (estimated)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Timeline Overview
|
||||||
|
|
||||||
|
Total estimated: **25-30 working days** (~6 minggu calendar, 1-2 minggu buffer)
|
||||||
|
|
||||||
|
```
|
||||||
|
Minggu 1 │ Sprint 0 ─ Sprint 1 ─ │──── Foundation
|
||||||
|
Minggu 2 │ Sprint 2 ─────────────│──── Core (Terminal)
|
||||||
|
Minggu 3 │ Sprint 3 ─────────────│──── SFTP + Polish
|
||||||
|
Minggu 4 │ Sprint 4 ─────────────│──── Advanced
|
||||||
|
Minggu 5 │ Sprint 5 ── Buffer ───│──── Polish + Release
|
||||||
|
└────────────────────────┘
|
||||||
|
26 Jun 10 Jul 24 Jul 7 Aug
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Detailed Timeline
|
||||||
|
|
||||||
|
### Phase 0: Foundation (Sprint 0 + Sprint 1)
|
||||||
|
**6 working days — 30 Jun s.d. 7 Jul**
|
||||||
|
|
||||||
|
| Day | Date | Sprint | Deliverable | Milestone |
|
||||||
|
|-----|------|--------|-------------|-----------|
|
||||||
|
| 1 | 30 Jun | Sprint 0 | Go module, Electron shell, Vite scaffold | App starts, shows "Hostkeeper V2" |
|
||||||
|
| 2 | 1 Jul | Sprint 0 | Health endpoint, middleware, build script | `curl /api/health` returns OK |
|
||||||
|
| 3 | 2 Jul | Sprint 1 | Host CRUD API (6 endpoints) | `curl POST /api/hosts` returns 201 |
|
||||||
|
| 4 | 3 Jul | Sprint 1 | Key + Snippet CRUD API | All CRUD works via curl |
|
||||||
|
| 5 | 4 Jul | Sprint 1 | Groups + Config + Vault API | Group tree returned |
|
||||||
|
| 6 | 7 Jul | Sprint 1 | Web UI scaffold (layout, router, theme) | Navigation works, sidebar visible |
|
||||||
|
|
||||||
|
**Milestone M1 — API Complete (7 Jul)**
|
||||||
|
- All REST API endpoints functional
|
||||||
|
- Storage integration with V1 `pkg/storage`
|
||||||
|
- Web UI layout renders
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: Core Features (Sprint 2)
|
||||||
|
**5 working days — 8 Jul s.d. 14 Jul**
|
||||||
|
|
||||||
|
| Day | Date | Deliverable | Milestone |
|
||||||
|
|-----|------|-------------|-----------|
|
||||||
|
| 7 | 8 Jul | Host List screen + HostForm UI | Add/edit host from browser |
|
||||||
|
| 8 | 9 Jul | HostForm: auth, tags, group picker | All form fields work |
|
||||||
|
| 9 | 10 Jul | WebSocket handler (Go) | Terminal WS connects |
|
||||||
|
| 10 | 11 Jul | XTermWrapper + terminal UI | Terminal renders, keystrokes work |
|
||||||
|
| 11 | 14 Jul | TabBar + multi-tab + snippet panel | Multi-tab terminal with snippets |
|
||||||
|
|
||||||
|
**Milestone M2 — Terminal Works (14 Jul)**
|
||||||
|
- SSH terminal via WebSocket
|
||||||
|
- Multi-tab support
|
||||||
|
- Snippet injection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: SFTP + Security (Sprint 3)
|
||||||
|
**5 working days — 15 Jul s.d. 21 Jul**
|
||||||
|
|
||||||
|
| Day | Date | Deliverable | Milestone |
|
||||||
|
|-----|------|-------------|-----------|
|
||||||
|
| 12 | 15 Jul | SFTP REST handlers (ls, upload, download) | SFTP API works via curl |
|
||||||
|
| 13 | 16 Jul | SFTP: mkdir, rm, rename, chmod | File operations work |
|
||||||
|
| 14 | 17 Jul | SFTPScreen + dual pane UI | SFTP browser functional |
|
||||||
|
| 15 | 18 Jul | Vault unlock + known hosts dialog | Security flow works |
|
||||||
|
| 16 | 21 Jul | Settings screen (all tabs) | Settings persist |
|
||||||
|
|
||||||
|
**Milestone M3 — SFTP + Vault (21 Jul)**
|
||||||
|
- SFTP file browser with upload/download
|
||||||
|
- Vault lock/unlock flow
|
||||||
|
- Settings customization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Advanced Features (Sprint 4)
|
||||||
|
**5 working days — 22 Jul s.d. 28 Jul**
|
||||||
|
|
||||||
|
| Day | Date | Deliverable | Milestone |
|
||||||
|
|-----|------|-------------|-----------|
|
||||||
|
| 17 | 22 Jul | PortForward CRUD + local tunnel | Local port forward works |
|
||||||
|
| 18 | 23 Jul | Remote + dynamic forwarding | All forward types work |
|
||||||
|
| 19 | 24 Jul | Import: SSH config + CSV parser | Bulk import works |
|
||||||
|
| 20 | 25 Jul | Export: all data encrypted | Export/import round-trip |
|
||||||
|
| 21 | 28 Jul | Jump host + proxy support | Multi-hop SSH works |
|
||||||
|
|
||||||
|
**Milestone M4 — Advanced Complete (28 Jul)**
|
||||||
|
- Port forwarding (local/remote/dynamic)
|
||||||
|
- Import/export
|
||||||
|
- Jump hosts + proxy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Polish & Release (Sprint 5)
|
||||||
|
**5 working days — 29 Jul s.d. 4 Aug**
|
||||||
|
|
||||||
|
| Day | Date | Deliverable | Milestone |
|
||||||
|
|-----|------|-------------|-----------|
|
||||||
|
| 22 | 29 Jul | Electron: menu, tray, shortcuts | Desktop polish complete |
|
||||||
|
| 23 | 30 Jul | OS Keychain + auto-update | Keychain integration |
|
||||||
|
| 24 | 31 Jul | Performance optimization | 1000 hosts smooth, memory < 200MB |
|
||||||
|
| 25 | 1 Aug | Android gomobile + WebView | Android app runs |
|
||||||
|
| 26 | 4 Aug | iOS gomobile + WKWebView | iOS app runs |
|
||||||
|
|
||||||
|
**Milestone M5 — Release Candidate (4 Aug)**
|
||||||
|
- All features complete
|
||||||
|
- Desktop and mobile builds
|
||||||
|
- Performance targets met
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Buffer: 3 days (5 Aug s.d. 7 Aug)
|
||||||
|
|
||||||
|
| Day | Date | Activity |
|
||||||
|
|-----|------|----------|
|
||||||
|
| 27 | 5 Aug | Bug fixes from integration testing |
|
||||||
|
| 28 | 6 Aug | Documentation finalization |
|
||||||
|
| 29 | 7 Aug | Release v2.0.0 |
|
||||||
|
|
||||||
|
**Milestone M6 — v2.0.0 Release (7 Aug)**
|
||||||
|
- All platforms built and signed
|
||||||
|
- Release notes complete
|
||||||
|
- Artifacts uploaded to GitHub
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Critical Path
|
||||||
|
|
||||||
|
```
|
||||||
|
Sprint 0 ── Sprint 1 ── Sprint 2 ── Sprint 5 ── Release
|
||||||
|
1 day 5 days 5 days 5 days 3 days
|
||||||
|
─────────────────────────────────────────────────────────
|
||||||
|
19 days minimum (no parallel work)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parallel Work Opportunities
|
||||||
|
|
||||||
|
| Sprint | Can run in parallel with | Notes |
|
||||||
|
|--------|-------------------------|-------|
|
||||||
|
| Sprint 1 | — | Must complete before Sprint 2 |
|
||||||
|
| Sprint 2 | — | Must complete before Sprint 3/4/5 |
|
||||||
|
| Sprint 3 | Sprint 4 | SFTP ≠ Port forwarding, different files |
|
||||||
|
| Sprint 4 | Sprint 3 | Independent features |
|
||||||
|
| Sprint 5 | Sprint 3, Sprint 4 | Desktop polish can start early |
|
||||||
|
|
||||||
|
**Optimistic timeline** (with parallelism): 22 days
|
||||||
|
**Conservative timeline** (sequential): 29 days
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Dependency Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
Sprint 0 (Scaffolding)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Sprint 1 (Foundation API)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Sprint 2 (SSH Terminal) ⭐ CRITICAL PATH
|
||||||
|
│
|
||||||
|
├────▶ Sprint 3 (SFTP + Security) ────▶ Sprint 5 (Desktop Polish)
|
||||||
|
│ │
|
||||||
|
└────▶ Sprint 4 (Advanced) ────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Release v2.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical path**: Sprint 0 → Sprint 1 → Sprint 2 → Sprint 5 → Release
|
||||||
|
|
||||||
|
**Fastest possible**: 22 hari (Sprint 3 + 4 done in parallel with Sprint 5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Milestone Summary
|
||||||
|
|
||||||
|
| Milestone | Date | Deliverable | Dependency |
|
||||||
|
|-----------|------|-------------|------------|
|
||||||
|
| **M1** | 7 Jul | All REST API endpoints + Web UI scaffold | Sprint 0 + Sprint 1 |
|
||||||
|
| **M2** | 14 Jul | SSH terminal with multi-tab | M1 |
|
||||||
|
| **M3** | 21 Jul | SFTP browser + vault + settings | M2 |
|
||||||
|
| **M4** | 28 Jul | Port forwarding + import/export + jump hosts | M2 |
|
||||||
|
| **M5** | 4 Aug | Desktop polish + mobile apps | M2, M3, M4 |
|
||||||
|
| **M6** | 7 Aug | **v2.0.0 Release** | M5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Weekly Breakdown
|
||||||
|
|
||||||
|
### Week 1 (30 Jun - 4 Jul) — Foundation
|
||||||
|
```
|
||||||
|
Mon 30: [Sprint 0] Go scaffold + Electron shell
|
||||||
|
Tue 1: [Sprint 0] Build script + middleware
|
||||||
|
Wed 2: [Sprint 1] Host CRUD API
|
||||||
|
Thu 3: [Sprint 1] Key + Snippet CRUD API
|
||||||
|
Fri 4: [Sprint 1] Groups + Config + Vault API
|
||||||
|
```
|
||||||
|
|
||||||
|
### Week 2 (7 Jul - 11 Jul) — API + UI Scaffold
|
||||||
|
```
|
||||||
|
Mon 7: [Sprint 1] Web UI scaffold (layout, router, theme) ← M1
|
||||||
|
Tue 8: [Sprint 2] Host List screen + HostForm
|
||||||
|
Wed 9: [Sprint 2] HostForm complete (auth, tags, groups)
|
||||||
|
Thu 10: [Sprint 2] WebSocket handler (Go)
|
||||||
|
Fri 11: [Sprint 2] XTermWrapper + terminal rendering
|
||||||
|
```
|
||||||
|
|
||||||
|
### Week 3 (14 Jul - 18 Jul) — Terminal + SFTP
|
||||||
|
```
|
||||||
|
Mon 14: [Sprint 2] TabBar + multi-tab + snippet panel ← M2
|
||||||
|
Tue 15: [Sprint 3] SFTP REST handlers
|
||||||
|
Wed 16: [Sprint 3] File operations (mkdir, rm, rename)
|
||||||
|
Thu 17: [Sprint 3] SFTPScreen + dual pane UI
|
||||||
|
Fri 18: [Sprint 3] Vault unlock + known hosts dialog
|
||||||
|
```
|
||||||
|
|
||||||
|
### Week 4 (21 Jul - 25 Jul) — SFTP Settings + Advanced
|
||||||
|
```
|
||||||
|
Mon 21: [Sprint 3] Settings screen (all tabs) ← M3
|
||||||
|
Tue 22: [Sprint 4] PortForward CRUD + local tunnel
|
||||||
|
Wed 23: [Sprint 4] Remote + dynamic forwarding
|
||||||
|
Thu 24: [Sprint 4] Import (SSH config, CSV)
|
||||||
|
Fri 25: [Sprint 4] Export + jump hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Week 5 (28 Jul - 1 Aug) — Advanced + Polish
|
||||||
|
```
|
||||||
|
Mon 28: [Sprint 4] Proxy support ← M4
|
||||||
|
Tue 29: [Sprint 5] Electron: menu, tray, shortcuts
|
||||||
|
Wed 30: [Sprint 5] OS Keychain + auto-update
|
||||||
|
Thu 31: [Sprint 5] Performance optimization
|
||||||
|
Fri 1: [Sprint 5] Android gomobile + WebView
|
||||||
|
```
|
||||||
|
|
||||||
|
### Week 6 (4 Aug - 7 Aug) — Mobile + Release
|
||||||
|
```
|
||||||
|
Mon 4: [Sprint 5] iOS gomobile + WKWebView ← M5
|
||||||
|
Tue 5: [Buffer] Bug fixes
|
||||||
|
Wed 6: [Buffer] Documentation
|
||||||
|
Thu 7: [Release] v2.0.0 ← M6
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Risk Buffer
|
||||||
|
|
||||||
|
| Risk | Impact | Probability | Mitigation | Buffer needed |
|
||||||
|
|------|--------|-------------|------------|---------------|
|
||||||
|
| Terminal WebSocket complexity | High | Medium | Use proven pattern from docs | +1 day |
|
||||||
|
| Mobile gomobile compilation | Medium | Medium | Start early, test on emulator | +1 day |
|
||||||
|
| Electron auto-update signing | Medium | Low | Prepare certificates early | — |
|
||||||
|
| Performance issues (1000 hosts) | Medium | Low | Virtual scrolling already planned | — |
|
||||||
|
| Unknown V1 package compatibility | Low | Low | V1 packages are framework-agnostic | — |
|
||||||
|
| UX polish takes longer | Medium | Medium | Prioritize function over beauty | +1 day |
|
||||||
|
|
||||||
|
**Total buffer**: 3 days (included in timeline as 5-7 Aug)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Effort by Technology
|
||||||
|
|
||||||
|
| Technology | Days | % of total | Sprint |
|
||||||
|
|------------|------|------------|--------|
|
||||||
|
| Go (backend) | 10 | 34% | S0, S1, S2, S3, S4 |
|
||||||
|
| React/TypeScript (frontend) | 10 | 34% | S1, S2, S3, S4, S5 |
|
||||||
|
| Electron (desktop) | 3 | 10% | S0, S5 |
|
||||||
|
| Mobile (gomobile + WebView) | 2 | 7% | S3, S5 |
|
||||||
|
| DevOps (build, CI/CD) | 2 | 7% | S0, S5 |
|
||||||
|
| Testing + bug fixes | 2 | 7% | All |
|
||||||
|
| **Total** | **29** | **100%** | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. What NOT in Scope (Post-MVP)
|
||||||
|
|
||||||
|
| Feature | Reason |
|
||||||
|
|---------|--------|
|
||||||
|
| Cloud sync | Requires backend server, Phase 3 |
|
||||||
|
| Team vault | Requires multi-user auth, Phase 3 |
|
||||||
|
| Multiplayer terminal | Requires relay server, Phase 3 |
|
||||||
|
| AI command generation | LLM API integration, nice-to-have |
|
||||||
|
| Serial console | Niche use case, low priority |
|
||||||
|
| Mosh support | Requires additional library |
|
||||||
|
| Telnet support | Legacy protocol, low priority |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Progress Tracking
|
||||||
|
|
||||||
|
**How to update this timeline**:
|
||||||
|
1. At the end of each day, mark `[x]` for completed tasks
|
||||||
|
2. If a task takes longer, adjust subsequent dates
|
||||||
|
3. Update `docs/v2/PROGRESS.md` with actual completion dates
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Week 1 Progress
|
||||||
|
[x] Mon 30 Jun: [Sprint 0] Go scaffold + Electron shell
|
||||||
|
[ ] Tue 1 Jul: [Sprint 0] Build script + middleware
|
||||||
|
[ ] Wed 2 Jul: [Sprint 1] Host CRUD API
|
||||||
|
[ ] Thu 3 Jul: [Sprint 1] Key + Snippet CRUD API
|
||||||
|
[ ] Fri 4 Jul: [Sprint 1] Groups + Config + Vault API
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Target**: v2.0.0 Release by **7 August 2026**
|
||||||
|
**Best case**: 22 July (parallel work, no blockers)
|
||||||
|
**Worst case**: 14 August (1 week buffer used + delays)
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user