docs: Finalize all design documents
Signed-off-by: Hermes Agent <hermes@nosuchhost>
This commit is contained in:
@@ -1,337 +1,232 @@
|
||||
# Shell & Logs Windows Feature Specification
|
||||
# Design Document: Shell & Logs Feature Specification
|
||||
|
||||
## Overview
|
||||
Shell and Logs windows are specialized detail windows that stream real-time data from Kubernetes pods. Shell provides an interactive terminal, while Logs shows container output.
|
||||
Shell and Logs are real-time terminal and streaming views for Kubernetes resources. These features enable users to interact with running pods and monitor application behavior.
|
||||
|
||||
---
|
||||
## Shell Terminal (kubectl exec)
|
||||
|
||||
## Shell Window
|
||||
### Requirements
|
||||
- **Implementation**: xterm.js on client
|
||||
- **Backend**: WebSocket → kubectl exec
|
||||
- **Protocol**: Bidirectional terminal protocol
|
||||
|
||||
### Purpose
|
||||
Interactive pseudo-terminal session inside the application, connected to a pod via `kubectl exec`.
|
||||
|
||||
### Implementation
|
||||
|
||||
#### Client-Side (xterm.js)
|
||||
### Terminal Configuration
|
||||
```typescript
|
||||
// Initialization
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
|
||||
const term = new Terminal({
|
||||
{
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
cursorBlink: true,
|
||||
theme: {
|
||||
background: '#0b0e13',
|
||||
foreground: '#e6edf6',
|
||||
cursor: '#4dd6e8'
|
||||
}
|
||||
});
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
fitAddon.fit();
|
||||
```
|
||||
|
||||
#### WebSocket Connection
|
||||
```typescript
|
||||
// On krate creation with shell tab
|
||||
const ws = new WebSocket(`/ws/shell?pod=${pod}&ns=${ns}`);
|
||||
|
||||
ws.onopen = () => {
|
||||
term.focus();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
term.write(event.data); // stdout/stderr
|
||||
};
|
||||
|
||||
term.onData((data) => {
|
||||
ws.send(data); // stdin (keypresses, etc.)
|
||||
});
|
||||
|
||||
term.onResize(({ rows, cols }) => {
|
||||
ws.send(JSON.stringify({ type: 'resize', rows, cols }));
|
||||
});
|
||||
```
|
||||
|
||||
### Keyboard Routing
|
||||
|
||||
#### Mouse-Over Focus
|
||||
- Track `mouseenter`/`mouseleave` per window
|
||||
- Set `hoveredWindowId` on shell windows
|
||||
- Global `keydown` handler routes to shell when focused
|
||||
|
||||
#### Critical: Space Key
|
||||
- **Must NOT open spotlight** when shell has focus
|
||||
- Shell window handles space as input (terminal command)
|
||||
- Implementation:
|
||||
```typescript
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (hoveredWindowId && isShellWindow(hoveredWindowId)) {
|
||||
// Space goes to shell
|
||||
return; // Don't trigger canvas shortcuts
|
||||
}
|
||||
// Handle canvas shortcuts (space pan, etc.)
|
||||
}, { capture: true });
|
||||
```
|
||||
|
||||
### Scroll Wheel Behavior
|
||||
- On shell content: Scroll terminal (not canvas)
|
||||
- On empty canvas: Pan
|
||||
- **Ctrl/⌘ + scroll over shell**: Zoom canvas (not terminal zoom)
|
||||
- Use `{ capture: true }` on canvas wheel handler
|
||||
- Check `e.ctrlKey || e.metaKey`
|
||||
- If true: zoom canvas; otherwise: let event propagate
|
||||
|
||||
### Terminal Features
|
||||
- **Full PTY emulation**: Support all ANSI escapes
|
||||
- **Resize**: Send new size on container resize
|
||||
- **Paste**: Support Ctrl+Shift+V or Shift+Insert
|
||||
- **Copy**: Ctrl+Shift+C or Ctrl+C (custom)
|
||||
- **Search**: `/` to search, `n`/`N` for next/prev
|
||||
- **Font scaling**: Ctrl+wheel for terminal font size
|
||||
|
||||
### Backend Implementation (Go)
|
||||
```go
|
||||
// WebSocket connection
|
||||
func (s *Server) handleShell(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
pod := r.URL.Query().Get("pod")
|
||||
ns := r.URL.Query().Get("ns")
|
||||
|
||||
// Create k8s exec request
|
||||
req := s.client.CoreV1().RESTClient().Post().
|
||||
Name(pod).Namespace(ns).Resource("pods").SubResource("exec")
|
||||
req.VersionedParams(&v1.ExecOptions{
|
||||
Stdin: true,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Terminal: true,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(s.config, "POST", req.URL())
|
||||
if err != nil {
|
||||
ws.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Stream between WebSocket and exec
|
||||
exec.Stream(remotecommand.StreamOptions{
|
||||
Stdin: ws,
|
||||
Stdout: ws,
|
||||
Stderr: ws,
|
||||
TermSize: &remotecommand.TerminalSize{
|
||||
Width: 80,
|
||||
Height: 24,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logs Window
|
||||
|
||||
### Purpose
|
||||
Live-tailing container logs, showing output in real-time.
|
||||
|
||||
### Streaming
|
||||
```typescript
|
||||
// WebSocket connection
|
||||
const ws = new WebSocket(`/ws/logs?pod=${pod}&ns=${ns}`);
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const line = event.data;
|
||||
addLogLine(line);
|
||||
};
|
||||
|
||||
function addLogLine(line: string) {
|
||||
const entry = parseLogLine(line);
|
||||
logBuffer.push(entry);
|
||||
|
||||
// Auto-scroll if at bottom
|
||||
if (atBottom()) {
|
||||
scrollToBottom();
|
||||
background: '#1a1e26',
|
||||
foreground: '#b9c6d8',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-Scroll Behavior
|
||||
- **New lines arrive**: Auto-scroll to bottom
|
||||
- **User scrolls up**: Stop auto-scrolling
|
||||
- **Detection**:
|
||||
```typescript
|
||||
function atBottom(): boolean {
|
||||
const threshold = 50; // px
|
||||
return (scrollTop + clientHeight + threshold) >= scrollHeight;
|
||||
}
|
||||
```
|
||||
### Connection Flow
|
||||
1. User opens shell view
|
||||
2. Extract pod name and namespace
|
||||
3. Connect WebSocket to `/ws/shell?pod=X&ns=Y`
|
||||
4. Backend:
|
||||
- Extract query params
|
||||
- Connect to k8s exec API
|
||||
- Create execstream
|
||||
- Proxy bidirectionally
|
||||
5. Frontend:
|
||||
- Initialize xterm
|
||||
- Attach to WebSocket
|
||||
- Handle resize events
|
||||
- Route keypresses
|
||||
|
||||
### Color Coding
|
||||
Parse lines and color-code by pattern:
|
||||
### Key Routing
|
||||
- **Shell focused + type**: Route to terminal
|
||||
- **Canvas shortcuts disabled** when shell has focus
|
||||
- **Space key**: Goes to terminal (not spotlight)
|
||||
- **Ctrl/⌘+scroll**: Zoom canvas (not terminal zoom)
|
||||
|
||||
```typescript
|
||||
function parseLogLine(line: string): LogEntry {
|
||||
// Pattern matching for common log levels
|
||||
if (line.includes('ERROR') || line.includes('ERR')) {
|
||||
return { text: line, color: '#ef6f6f' }; // Red
|
||||
} else if (line.includes('WARN') || line.includes('WARNING')) {
|
||||
return { text: line, color: '#e8b54a' }; // Amber
|
||||
} else if (line.includes('INFO') || line.includes('I ')) {
|
||||
return { text: line, color: '#b9c6d8' }; // Default
|
||||
} else {
|
||||
return { text: line, color: '#c7d2e0' }; // Muted
|
||||
}
|
||||
}
|
||||
```
|
||||
### Resize Handling
|
||||
- **Client**: term.resize(cols, rows)
|
||||
- **Server**: Send resize message via WebSocket
|
||||
- **Backend**: Update k8s exec term size
|
||||
- **Debounce**: 100ms for resize events
|
||||
|
||||
### Timestamps
|
||||
- **Default**: Include timestamps
|
||||
- **Toggle**: Optional button to show/hide
|
||||
- **Format**: ISO 8601 or container format
|
||||
|
||||
### Scrollbar Styling
|
||||
- Width: 8px
|
||||
- Track: rgba(140,165,200,.1)
|
||||
- Thumb: rgba(140,165,200,.3)
|
||||
- Hover thumb: rgba(140,165,200,.5)
|
||||
|
||||
### Content Rendering
|
||||
```typescript
|
||||
<pre style={{ overflow: 'auto', height: '286px', fontFamily: 'IBM Plex Mono' }}>
|
||||
{logs.map((line, i) => (
|
||||
<div key={i} style={{ color: line.color }}>
|
||||
{line.timestamp && <span className="timestamp">{line.timestamp} </span>}
|
||||
{line.text}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## View-Specific Behavior
|
||||
|
||||
### Shell Tab
|
||||
- **Enabled for**: Pods, and workloads that support exec
|
||||
- **Disabled for**: Deployments, Services, etc.
|
||||
- **Visual**: `opacity: 0.45` for disabled
|
||||
- **Label**: Show "shell" but don't route to it
|
||||
|
||||
### Logs Tab
|
||||
- **Enabled for**: Pods and workloads ( Deployments, StatefulSets, DaemonSets)
|
||||
- **Disabled for**: ConfigMaps, Secrets, PVCs, etc.
|
||||
- **Visual**: Same as shell
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Key | Context | Action |
|
||||
|---|---|---|
|
||||
| ↑ / ↓ | Logs shell focused | Navigate scroll (logs) / line history (shell) |
|
||||
| PageUp / PageDown | Logs focused | Page scroll |
|
||||
| Ctrl+L | Logs focused | Clear screen (only in shell) |
|
||||
| Ctrl+C | Shell focused | Send interrupt to shell |
|
||||
| Ctrl+Z | Shell focused | Send suspend (if supported) |
|
||||
| Ctrl+Shift+F | Logs focused | Find (future enhancement) |
|
||||
| Ctrl+Shift+V | Shell focused | Paste |
|
||||
| Ctrl+Shift+C | Shell focused | Copy selection |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Connection Errors
|
||||
- **Shell**: Show error border, keep terminal buffer, retry button
|
||||
- **Logs**: Show error indicator, auto-reconnect with backoff
|
||||
|
||||
### Reconnection Strategy
|
||||
```typescript
|
||||
let retries = 0;
|
||||
const maxRetries = 5;
|
||||
|
||||
function reconnect() {
|
||||
if (retries >= maxRetries) {
|
||||
showFatalError('Connection failed');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
retries++;
|
||||
connect();
|
||||
}, Math.min(1000 * 2^retries, 10000)); // Exponential backoff
|
||||
}
|
||||
```
|
||||
|
||||
### Buffer Management
|
||||
- **Shell**: Keep last 1000 lines in buffer
|
||||
- **Logs**: Keep last 2000 lines
|
||||
- **Overflow**: Drop oldest lines, mark with "..." indicator
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### Shell State
|
||||
### State Management
|
||||
```typescript
|
||||
interface ShellState {
|
||||
connected: boolean;
|
||||
rows: number;
|
||||
connectedAt: number;
|
||||
lastActive: number;
|
||||
buffer: string[];
|
||||
cols: number;
|
||||
buffer: string[]; // Last N lines
|
||||
cursor: { x, y };
|
||||
rows: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Logs State
|
||||
### Error States
|
||||
- Connection failed: Show error message
|
||||
- Disconnected: Show "Disconnected" + reconnect button
|
||||
- Permission denied: Show clear error
|
||||
- Pod not found: Show error message
|
||||
|
||||
## Logs Viewer (kubectl logs)
|
||||
|
||||
### Requirements
|
||||
- **Implementation**: WebSocket streaming
|
||||
- **Auto-scroll**: To bottom by default
|
||||
- **Color coding**: ERROR/WARN/INFO
|
||||
|
||||
### Connection Flow
|
||||
1. User opens logs view
|
||||
2. Connect to `/ws/logs?pod=X&ns=Y`
|
||||
3. Backend:
|
||||
- Start kubectl logs stream
|
||||
- Forward to WebSocket
|
||||
4. Frontend:
|
||||
- Append lines to buffer
|
||||
- Auto-scroll if at bottom
|
||||
- Colorize based on level
|
||||
|
||||
### Log Parsing
|
||||
Each log line should include:
|
||||
- Timestamp
|
||||
- Log level (ERROR/WARN/INFO/DEBUG)
|
||||
- Message
|
||||
|
||||
Format:
|
||||
```typescript
|
||||
interface LogLine {
|
||||
timestamp: string; // ISO 8601
|
||||
level: 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
|
||||
message: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Color Coding
|
||||
- **ERROR**: `#ef6f6f`
|
||||
- **WARN**: `#e8b54a`
|
||||
- **INFO**: `#b9c6d8`
|
||||
- **DEBUG**: `#6b7280`
|
||||
|
||||
### Auto-Scroll Logic
|
||||
- **Track scroll position**: Store `scrollTop`
|
||||
- **At bottom**: Auto-scroll on new log
|
||||
- **Scrolled up**: Don't auto-scroll
|
||||
- **Threshold**: Within 20px of bottom = "at bottom"
|
||||
|
||||
### User Controls
|
||||
- **Click auto-scroll toggle**: Switch between auto and manual scroll
|
||||
- **Clear logs**: Button to clear buffer
|
||||
- **Copy log line**: Right-click menu
|
||||
- **Filter**: Simple filtering (show/hide levels)
|
||||
|
||||
### State Management
|
||||
```typescript
|
||||
interface LogsState {
|
||||
lines: LogEntry[];
|
||||
scrollTop: number;
|
||||
connected: boolean;
|
||||
autoScroll: boolean;
|
||||
timestamp: boolean;
|
||||
logLines: LogLine[];
|
||||
scrollPosition: number;
|
||||
filterLevels: Set<LogLevel>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### Streaming Considerations
|
||||
- **Backpressure**: Buffer if client slow
|
||||
- **Rate limiting**: Server can throttle logs
|
||||
- **Keep-alive**: Ping every 30s
|
||||
- **Reconnect**: Exponential backoff on disconnect
|
||||
|
||||
### Performance Optimization
|
||||
- **Virtualization**: Only render visible lines
|
||||
- **Batching**: Flush logs in batches if high volume
|
||||
- **Throttling**: 16ms throttle for updates
|
||||
- **Cleanup**: Remove old logs (keep last 1000 lines)
|
||||
|
||||
## Shared Features
|
||||
|
||||
### View State
|
||||
- Both views persist state in krate.window.state
|
||||
- reconnect: Auto reconnect on disconnect
|
||||
- timestamps: Show/hide timestamps
|
||||
- wrap: Word wrap toggle
|
||||
|
||||
### Tab Switching
|
||||
- Shell tab: Enabled for pods only
|
||||
- Logs tab: Enabled for pods/workloads
|
||||
- Empty state: Show helpful message for unsupported resources
|
||||
|
||||
### Close Behavior
|
||||
- Shell: Keep buffer, show "Disconnnected"
|
||||
- Logs: Stop streaming, keep buffer
|
||||
- Cleanup: Close WebSocket, cancel watchers
|
||||
|
||||
## WebSocket Protocol
|
||||
|
||||
### Shell
|
||||
```
|
||||
// Client → Server
|
||||
{
|
||||
type: 'shell:input',
|
||||
data: string // Key press data
|
||||
}
|
||||
|
||||
{
|
||||
type: 'shell:resize',
|
||||
cols: number,
|
||||
rows: number
|
||||
}
|
||||
|
||||
// Server → Client
|
||||
{
|
||||
type: 'shell:output',
|
||||
data: string // stdout/stderr
|
||||
}
|
||||
|
||||
{
|
||||
type: 'shell:status',
|
||||
connected: boolean,
|
||||
message?: string
|
||||
}
|
||||
```
|
||||
|
||||
### Logs
|
||||
```
|
||||
// Client → Server
|
||||
{
|
||||
type: 'logs:subscribe',
|
||||
pod: string,
|
||||
namespace: string,
|
||||
follow: boolean,
|
||||
timestamps: boolean
|
||||
}
|
||||
|
||||
{
|
||||
type: 'logs:filter',
|
||||
levels: string[]
|
||||
}
|
||||
|
||||
// Server → Client
|
||||
{
|
||||
type: 'log',
|
||||
log: LogLine
|
||||
}
|
||||
|
||||
{
|
||||
type: 'status',
|
||||
connected: boolean,
|
||||
message?: string
|
||||
}
|
||||
```
|
||||
|
||||
## Gotchas
|
||||
|
||||
### Critical Issues
|
||||
1. **Space key routing**: Must distinguish between shell space (input) and canvas space-pan
|
||||
2. **Ctrl/⌘+scroll**: Intercept on canvas with `{ capture: true }`, check modifier
|
||||
3. **Terminal resize**: Send `TermSize` on window resize events
|
||||
4. **Paste security**: Sanitize pasted content, avoid escape sequences
|
||||
5. **Cursor position**: xterm.js tracks cursor; keep in sync with server
|
||||
6. **Buffer overflow**: Implement LRU buffer, don't memory leak
|
||||
|
||||
### Performance
|
||||
1. **Throttle updates**: Don't render on every log line, batch
|
||||
2. **Virtual scrolling**: For logs with many lines
|
||||
3. **Web Worker**: Offload parsing to worker for large logs
|
||||
|
||||
### Animation
|
||||
1. **Blinking cursor**: CSS animation or xterm.js built-in
|
||||
2. **Auto-scroll indicator**: Small arrow when scrolled up
|
||||
3. **Connection status**: Green/amber/red dot
|
||||
|
||||
## Testing Checklist
|
||||
- [ ] Shell key press → terminal receives it
|
||||
- [ ] Terminal output → WebSocket → UI
|
||||
- [ ] Resize → terminal resizes correctly
|
||||
- [ ] Space in shell ≠ spotlight open
|
||||
- [ ] Ctrl+scroll over shell ≠ zoom canvas
|
||||
- [ ] Logs auto-scroll works (and stops on manual scroll)
|
||||
- [ ] Connection loss → retry with backoff
|
||||
- [ ] Buffer overflow → old lines dropped
|
||||
- [ ] Shell interrupt (Ctrl+C) handled
|
||||
- [ ] Paste works (Ctrl+Shift+V)
|
||||
1. **Space key**: Must distinguish between canvas pan and shell input
|
||||
2. **Focus routing**: Route keys based on hovered window
|
||||
3. **Scroll management**: Track scroll position for logs
|
||||
4. **Terminal resize**: Debounce resize events
|
||||
5. **Reconnection**: Handle WebSocket disconnects gracefully
|
||||
6. **Memory**: Clean up old logs/shell buffers
|
||||
7. **Authentication**: Validate user on WebSocket upgrade
|
||||
8. **Permissions**: Enforce namespace RBAC on backend
|
||||
|
||||
Reference in New Issue
Block a user