chore: V2 planning docs + template discovery
This commit is contained in:
@@ -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** |
|
||||
Reference in New Issue
Block a user