# 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** |