Files
HostKeeper/docs/SPRINT_PLAN.md
T
swanadiva c215d68c81 refactor: flatten docs/ — move V2 docs from docs/v2/ to docs/
- git mv docs/v2/*.md docs/ (11 planning documents)
- Remove empty docs/v2/ directory
- Update all references: AGENTS.md, CHANGELOG.md, README.md, ARCHITECTURE.md
- Fix outdated repo tree in ARCHITECTURE.md to match actual structure
2026-07-07 12:04:59 +07:00

19 KiB

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:

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:

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:

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:

#!/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):

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):

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