feat: Add comprehensive design documentation for Krates
- Canvas: Infinite zoomable workspace with LOD and navigation - Spotlight: Fuzzy search with type filters and view shortcuts - Krate: Window group container with non-overlapping placement - Detail Window: YAML/Describe/Logs/Shell views with maximize - Top Bar: Cluster info, user presence, admin toggle - Admin Drawer: Multi-user presence and spectate functionality - Minimap: Browse and navigate canvas overview - Collection Window: List/tree views with filtering and sorting - Shell/Logs: Real-time terminal and log streaming - Backend: Go service with K8s API, WebSocket handlers, CRDT sync - Architecture: Full project structure and tech stack
This commit is contained in:
337
design/shell-logs.md
Normal file
337
design/shell-logs.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Shell & Logs Windows 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 Window
|
||||
|
||||
### Purpose
|
||||
Interactive pseudo-terminal session inside the application, connected to a pod via `kubectl exec`.
|
||||
|
||||
### Implementation
|
||||
|
||||
#### Client-Side (xterm.js)
|
||||
```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,
|
||||
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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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;
|
||||
}
|
||||
```
|
||||
|
||||
### Color Coding
|
||||
Parse lines and color-code by pattern:
|
||||
|
||||
```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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
```typescript
|
||||
interface ShellState {
|
||||
connected: boolean;
|
||||
rows: number;
|
||||
cols: number;
|
||||
buffer: string[]; // Last N lines
|
||||
cursor: { x, y };
|
||||
}
|
||||
```
|
||||
|
||||
### Logs State
|
||||
```typescript
|
||||
interface LogsState {
|
||||
lines: LogEntry[];
|
||||
scrollTop: number;
|
||||
autoScroll: boolean;
|
||||
timestamp: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
Reference in New Issue
Block a user