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
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user