c215d68c81
- 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
392 lines
12 KiB
Markdown
392 lines
12 KiB
Markdown
# Hostkeeper V2 — Extended Data Models
|
|
|
|
> **Status**: V2 Planning Complete
|
|
> **Last Updated**: 2026-06-29
|
|
> **V1 models**: `internal/models/models.go` — frozen, no changes.
|
|
|
|
---
|
|
|
|
## 1. Overview
|
|
|
|
V2 extends V1 models with new entities for groups, vault, port forwarding, workspaces, and sessions. All V1 models remain unchanged. New models are added in `app/backend/models/` (or `internal/models/` with backward-compatible additions).
|
|
|
|
---
|
|
|
|
## 2. V1 Models (FROZEN — do not modify)
|
|
|
|
These exist in `internal/models/models.go` and are used as-is:
|
|
|
|
- `Host` — SSH host connection configuration
|
|
- `AuthConfig` — Authentication configuration (password/key/agent)
|
|
- `KeyPair` — SSH key pair
|
|
- `Snippet` — Command snippet
|
|
- `Profile` — Named configuration profile
|
|
- `AppConfig` — Application configuration
|
|
- `KnownHost` — Verified host key
|
|
|
|
---
|
|
|
|
## 3. V2 Models (NEW)
|
|
|
|
### 3.1 HostGroup
|
|
|
|
Nestable group for organizing hosts. Groups can contain other groups and hosts.
|
|
|
|
```go
|
|
type HostGroup struct {
|
|
ID string `json:"id" bson:"_id,omitempty"`
|
|
Name string `json:"name"`
|
|
ParentID *string `json:"parent_id,omitempty"` // nil = root group
|
|
Color string `json:"color,omitempty"` // hex color e.g. "#e78a4e"
|
|
Icon string `json:"icon,omitempty"` // icon name e.g. "server", "globe"
|
|
Order int `json:"order"` // sort order within parent
|
|
Children []*HostGroup `json:"children,omitempty"` // populated at runtime, not stored
|
|
HostCount int `json:"host_count,omitempty"` // populated at runtime
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
```
|
|
|
|
**Storage**: `groups.json`
|
|
```json
|
|
{
|
|
"groups": [
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"name": "Production",
|
|
"parent_id": null,
|
|
"color": "#e78a4e",
|
|
"icon": "server",
|
|
"order": 0,
|
|
"created_at": "2024-06-22T10:00:00Z",
|
|
"updated_at": "2024-06-22T10:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Rules**:
|
|
- Maximum nesting depth: 5 levels
|
|
- Root groups have `parent_id: null`
|
|
- Deleting a group moves its hosts to parent (or ungrouped)
|
|
- Group names must be unique within the same parent
|
|
|
|
---
|
|
|
|
### 3.2 Extended Host
|
|
|
|
New fields added to the existing `Host` struct:
|
|
|
|
```go
|
|
// V2 additions to Host struct
|
|
type HostV2 struct {
|
|
// ... all V1 fields ...
|
|
|
|
GroupID *string `json:"group_id,omitempty"` // reference to HostGroup.ID
|
|
IsFavorite bool `json:"is_favorite"` // quick access
|
|
Color string `json:"color,omitempty"` // override group color
|
|
Order int `json:"order"` // sort order within group
|
|
StartupSnippets []string `json:"startup_snippets,omitempty"` // snippet IDs to run on connect
|
|
Proxy *ProxyConfig `json:"proxy,omitempty"` // proxy configuration
|
|
JumpHost *string `json:"jump_host_id,omitempty"` // jump host ID
|
|
Keepalive int `json:"keepalive,omitempty"` // keepalive interval in seconds
|
|
}
|
|
```
|
|
|
|
**Note**: V1 Host fields are untouched. V2 fields are added with `omitempty` for backward compatibility.
|
|
|
|
---
|
|
|
|
### 3.3 ProxyConfig
|
|
|
|
Proxy configuration for SSH connections through proxy servers.
|
|
|
|
```go
|
|
type ProxyConfig struct {
|
|
Type string `json:"type"` // "socks5", "http", "command"
|
|
Host string `json:"host"` // proxy host
|
|
Port int `json:"port"` // proxy port
|
|
Username string `json:"username,omitempty"`
|
|
Password string `json:"password,omitempty"`
|
|
Command string `json:"command,omitempty"` // for "command" type (netcat-style)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3.4 PortForward
|
|
|
|
SSH port forwarding configuration.
|
|
|
|
```go
|
|
type PortForward struct {
|
|
ID string `json:"id" bson:"_id,omitempty"`
|
|
Name string `json:"name"`
|
|
HostID string `json:"host_id"` // reference to Host.ID
|
|
Type string `json:"type"` // "local", "remote", "dynamic"
|
|
LocalAddr string `json:"local_addr"` // e.g. "127.0.0.1:8080"
|
|
RemoteAddr string `json:"remote_addr"` // e.g. "localhost:3000"
|
|
Status string `json:"status"` // "stopped", "running", "error"
|
|
AutoStart bool `json:"auto_start"` // start tunnel on app launch
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
```
|
|
|
|
**Storage**: `forwards.json`
|
|
```json
|
|
{
|
|
"forwards": [
|
|
{
|
|
"id": "forward-uuid",
|
|
"name": "Local Redis",
|
|
"host_id": "host-uuid",
|
|
"type": "local",
|
|
"local_addr": "127.0.0.1:6379",
|
|
"remote_addr": "localhost:6379",
|
|
"status": "stopped",
|
|
"auto_start": false,
|
|
"created_at": "2024-06-22T10:00:00Z",
|
|
"updated_at": "2024-06-22T10:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Forward Types**:
|
|
- **Local**: `ssh -L localPort:remoteHost:remotePort` — forward local port to remote
|
|
- **Remote**: `ssh -R remotePort:localHost:localPort` — forward remote port to local
|
|
- **Dynamic**: `ssh -D localPort` — SOCKS5 proxy through SSH
|
|
|
|
---
|
|
|
|
### 3.5 Workspace
|
|
|
|
Group multiple terminal sessions in a layout.
|
|
|
|
```go
|
|
type Workspace struct {
|
|
ID string `json:"id" bson:"_id,omitempty"`
|
|
Name string `json:"name"`
|
|
Layout string `json:"layout"` // "single", "split-h", "split-v", "grid-2x2"
|
|
Tabs []Tab `json:"tabs"` // terminal tabs in this workspace
|
|
IsActive bool `json:"is_active"` // currently active workspace
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type Tab struct {
|
|
ID string `json:"id"`
|
|
HostID string `json:"host_id"`
|
|
Type string `json:"type"` // "terminal", "sftp"
|
|
Title string `json:"title"` // display title (host name)
|
|
Cols int `json:"cols"`
|
|
Rows int `json:"rows"`
|
|
}
|
|
```
|
|
|
|
**Storage**: `workspaces.json`
|
|
|
|
**Layout Types**:
|
|
- `single` — one terminal, full width
|
|
- `split-h` — two terminals side by side (horizontal split)
|
|
- `split-v` — two terminals stacked (vertical split)
|
|
- `grid-2x2` — four terminals in a 2x2 grid
|
|
|
|
---
|
|
|
|
### 3.6 Session (Runtime only, not persisted)
|
|
|
|
Active terminal session tracking.
|
|
|
|
```go
|
|
type Session struct {
|
|
ID string `json:"id"`
|
|
HostID string `json:"host_id"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
LastActive time.Time `json:"last_active"`
|
|
Status string `json:"status"` // "connecting", "connected", "disconnected", "error"
|
|
Cols int `json:"cols"`
|
|
Rows int `json:"rows"`
|
|
}
|
|
```
|
|
|
|
**Note**: Sessions are in-memory only. Not stored to disk.
|
|
|
|
---
|
|
|
|
### 3.7 VaultStatus (Runtime only)
|
|
|
|
Vault lock/unlock state tracking.
|
|
|
|
```go
|
|
type VaultStatus struct {
|
|
Locked bool `json:"locked"`
|
|
EncryptionEnabled bool `json:"encryption_enabled"`
|
|
AutoLockEnabled bool `json:"auto_lock_enabled"`
|
|
AutoLockMinutes int `json:"auto_lock_minutes"`
|
|
LastActivity time.Time `json:"last_activity"`
|
|
UnlockAttempts int `json:"unlock_attempts"` // failed attempts
|
|
LockedUntil *time.Time `json:"locked_until,omitempty"` // temporary lock after too many attempts
|
|
}
|
|
```
|
|
|
|
**Auto-lock rules**:
|
|
- Default: lock after 5 minutes of inactivity
|
|
- After 5 failed unlock attempts: temporary lock for 30 seconds
|
|
- Lock on system sleep/suspend
|
|
- Lock on window minimize (configurable)
|
|
|
|
---
|
|
|
|
## 4. Storage Schema
|
|
|
|
### File Structure
|
|
|
|
```
|
|
~/.hostkeeper/
|
|
├── hosts.json # V1 + V2 host data
|
|
├── keys.json # V1 + V2 key pairs
|
|
├── snippets.json # V1 + V2 snippets
|
|
├── groups.json # V2: host groups
|
|
├── forwards.json # V2: port forwards
|
|
├── workspaces.json # V2: workspaces
|
|
├── config.json # V1 + V2 app config
|
|
├── known_hosts.json # V1 + V2 known hosts
|
|
├── profiles.json # V2: profiles (if separated from config)
|
|
└── sessions.json # V2: session history (optional)
|
|
```
|
|
|
|
### JSON Schema Examples
|
|
|
|
#### hosts.json (extended)
|
|
|
|
```json
|
|
{
|
|
"hosts": [
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"name": "Production Server",
|
|
"hostname": "192.168.1.100",
|
|
"port": 22,
|
|
"username": "admin",
|
|
"auth": {
|
|
"type": "key",
|
|
"key_id": "key-uuid"
|
|
},
|
|
"group_id": "group-uuid",
|
|
"tags": ["production", "linux"],
|
|
"is_favorite": true,
|
|
"color": "#e78a4e",
|
|
"order": 0,
|
|
"notes": "Main production server",
|
|
"startup_snippets": ["snippet-uuid-1"],
|
|
"proxy": {
|
|
"type": "socks5",
|
|
"host": "10.0.0.1",
|
|
"port": 1080
|
|
},
|
|
"jump_host_id": "jump-host-uuid",
|
|
"keepalive": 60,
|
|
"created_at": "2024-06-22T10:00:00Z",
|
|
"updated_at": "2024-06-22T10:00:00Z",
|
|
"last_used_at": "2024-06-22T15:30:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### groups.json
|
|
|
|
```json
|
|
{
|
|
"groups": [
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"name": "Production",
|
|
"parent_id": null,
|
|
"color": "#e78a4e",
|
|
"icon": "server",
|
|
"order": 0,
|
|
"created_at": "2024-06-22T10:00:00Z",
|
|
"updated_at": "2024-06-22T10:00:00Z"
|
|
},
|
|
{
|
|
"id": "660e8400-e29b-41d4-a716-446655440001",
|
|
"name": "Web Servers",
|
|
"parent_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"color": "#a9b665",
|
|
"icon": "globe",
|
|
"order": 0,
|
|
"created_at": "2024-06-22T10:00:00Z",
|
|
"updated_at": "2024-06-22T10:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
#### forwards.json
|
|
|
|
```json
|
|
{
|
|
"forwards": [
|
|
{
|
|
"id": "forward-uuid",
|
|
"name": "Local Redis",
|
|
"host_id": "host-uuid",
|
|
"type": "local",
|
|
"local_addr": "127.0.0.1:6379",
|
|
"remote_addr": "localhost:6379",
|
|
"status": "stopped",
|
|
"auto_start": false,
|
|
"created_at": "2024-06-22T10:00:00Z",
|
|
"updated_at": "2024-06-22T10:00:00Z"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Relationships
|
|
|
|
```
|
|
HostGroup (1) ──── (*) Host
|
|
│ │
|
|
│ parent_id │ group_id
|
|
│ (self-ref) │ key_id
|
|
│ │ startup_snippets (*)
|
|
│ │ jump_host_id
|
|
│ │
|
|
HostGroup (1) ──── (*) HostGroup (children)
|
|
│
|
|
└── max depth: 5
|
|
|
|
Host (1) ──── (*) PortForward
|
|
│
|
|
└── host_id
|
|
|
|
Workspace (1) ──── (*) Tab
|
|
│
|
|
└── tabs[].host_id
|
|
|
|
Host (1) ──── (*) Snippet (via startup_snippets)
|
|
│
|
|
└── startup_snippets[] = snippet IDs
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Migration from V1
|
|
|
|
V2 data is backward-compatible with V1. New fields are optional (`omitempty`).
|
|
|
|
**Migration steps** (automatic on first V2 launch):
|
|
1. Read existing `hosts.json` — V2 fields default to zero values
|
|
2. Create `groups.json` if not exists — empty groups
|
|
3. Create `forwards.json` if not exists — empty forwards
|
|
4. Create `workspaces.json` if not exists — empty workspaces
|
|
5. Update `config.json` — add V2 config fields
|
|
|
|
**No data loss**: V1 data is preserved. V2 fields are additive.
|