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:
568
README.md
568
README.md
@@ -1,3 +1,567 @@
|
|||||||
# opencode-krates-connector
|
# Handoff: Krates — Kubernetes Visual Cluster Navigator
|
||||||
|
|
||||||
Design documentation for Krates Kubernetes cluster navigator
|
## Overview
|
||||||
|
|
||||||
|
Krates is a keyboard-first, search-driven Kubernetes cluster navigator. It gives operators and developers a fast, spatial way to explore any cluster: search for any object, open exactly the views you need (logs, shell, describe, YAML), and keep them organized as named **krates** on an infinite zoomable canvas. Multiple users share the same cluster workspace (the **yard**), and an admin view shows all users' live queries and open krates.
|
||||||
|
|
||||||
|
The product lives at **krates.io**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## About the Design Files
|
||||||
|
|
||||||
|
`Krates.dc.html` is a **high-fidelity interactive prototype** — it runs in a browser and is fully interactive. It is **not production code**. Its purpose is to demonstrate the intended look, behavior, keyboard model, and data shapes precisely enough that a developer can implement the real app without guessing.
|
||||||
|
|
||||||
|
**Your task:** Recreate this UI in a real production stack (recommended: React + TypeScript + a WebSocket/CRDT backend for real-time sync). Use the prototype as the authoritative reference for interactions, visual design, and UX decisions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fidelity
|
||||||
|
|
||||||
|
**High-fidelity.** Colors, typography, spacing, animations, and interaction behavior are all final. Implement pixel-accurately. The prototype uses real mock cluster data; the real app will use a Kubernetes API server (kubectl proxy, or a Go/Rust backend aggregating the k8s API).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Mental Model
|
||||||
|
|
||||||
|
| Term | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| **Yard** | The shared cluster workspace — one per connected cluster |
|
||||||
|
| **Krate** | A named group of windows for one object (or a collection); sits on the canvas |
|
||||||
|
| **Window** | A single view (Logs / Shell / Describe / YAML) for one object, inside a krate |
|
||||||
|
| **Collection window** | A category or namespace overview (all Pods, all Services, ns/payments…) with a filterable list |
|
||||||
|
| **Spotlight** | The global search overlay — opens by clicking anywhere or typing any key |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screens / Views
|
||||||
|
|
||||||
|
### 1. Canvas (main surface)
|
||||||
|
|
||||||
|
**Purpose:** Infinite zoomable/pannable workspace where krates live. The default starting point is empty — no pre-placed content.
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
- Full viewport, `position: fixed; inset: 0`
|
||||||
|
- Background: `#0b0e13`
|
||||||
|
- CAD grid overlay: two layers — fine (`34px`, rgba(125,145,175,.04)) and coarse (`170px`, rgba(125,145,175,.075)), both in X and Y. Rendered via CSS `background-image` repeating linear gradients.
|
||||||
|
- World layer: a large div (`12000×8000px`) transformed via `translate(camX, camY) scale(zoom)`. All krates and windows are absolutely positioned children of this layer.
|
||||||
|
- Top bar: `position: absolute; top: 0; left: 0; right: 0; height ~56px; z-index: 10`
|
||||||
|
- Bottom hint bar: `position: absolute; right: 18px; bottom: 18px`
|
||||||
|
- Zoom pill: `position: absolute; left: 18px; bottom: 18px`
|
||||||
|
- Minimap: `position: absolute; right: 18px; bottom: 64px; width: 180px; height: 120px`
|
||||||
|
|
||||||
|
**Navigation:**
|
||||||
|
- **Scroll wheel** = pan (default)
|
||||||
|
- **Ctrl/⌘ + scroll** = zoom (intercepted via `wheel` event, `e.ctrlKey || e.metaKey`)
|
||||||
|
- **Space + drag** = pan. Critical: space panning must work even when a shell window has keyboard focus. Implement by capturing `keydown` for space globally with `capture: true`, setting a "panning" flag, preventing default on the body, and forwarding `mousemove` to the canvas pan handler. Release on `keyup` for space.
|
||||||
|
- **Click on empty canvas** = open spotlight
|
||||||
|
- **Type any key** = open spotlight with that character pre-seeded
|
||||||
|
|
||||||
|
**Zoom levels / LOD (Level of Detail):**
|
||||||
|
- `zoom > 0.5`: normal view — krates expanded, all windows visible, draggable
|
||||||
|
- `zoom < 0.4`: collapsed view — krates become small overview cards (230px wide), no windows rendered. **Hysteresis:** collapse at 0.4, re-expand at 0.5 to prevent flickering during a single scroll gesture.
|
||||||
|
- The camera uses CSS `transform` with a `transition` only during programmatic "fly" animations (`.52s cubic-bezier(.22,.8,.28,1)`). During user scroll/drag, transition is `none` for immediacy.
|
||||||
|
|
||||||
|
### 2. Top Bar
|
||||||
|
|
||||||
|
**Layout:** `display: flex; align-items: center; padding: 13px 18px`
|
||||||
|
**Background:** none (transparent over canvas)
|
||||||
|
|
||||||
|
Left side (left→right):
|
||||||
|
- **Logo pill**: `krates / yard` — `background: rgba(14,18,25,.82); border: 1px solid rgba(140,165,200,.18); border-radius: 9px; padding: 7px 12px; backdrop-filter: blur(6px)`. The diamond glyph is `clip-path: polygon(50% 0,100% 50%,50% 100%,0 50%)` in accent color.
|
||||||
|
- **Cluster pill**: cluster name + green health dot (`#4ad07a`, `box-shadow: 0 0 8px #4ad07a`)
|
||||||
|
- **Krate count pill**: only shown when krates exist
|
||||||
|
|
||||||
|
Right side:
|
||||||
|
- **Synced pill**: pulsing accent dot + "synced" text
|
||||||
|
- **Admin button**: `◉ admin` — accent-outlined when active, muted when inactive
|
||||||
|
- **Roster avatars**: overlapping circles (`margin-left: -7px`), each 30×30px, colored per user, showing 2-letter initials (★ for self)
|
||||||
|
|
||||||
|
### 3. Spotlight Search
|
||||||
|
|
||||||
|
**Purpose:** Global search overlay. Fuzzy-ranked results, Tab type-filter, view shortcuts on selected result.
|
||||||
|
|
||||||
|
**Trigger:** Click anywhere on empty canvas, or type any printable character (pre-seeds the query).
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
- Full-screen backdrop: `rgba(7,9,13,.55)`, `backdrop-filter: blur(2px)`
|
||||||
|
- Search panel: `position: absolute; left: 50%; top: 15%; transform: translateX(-50%); width: min(660px, 93vw)`
|
||||||
|
- Panel: `background: rgba(16,20,28,.97); border: 1px solid rgba(140,165,200,.26); border-radius: 14px; overflow: hidden`
|
||||||
|
- Input row height: ~52px, with `font-size: 18px`, IBM Plex Sans
|
||||||
|
|
||||||
|
**Sections (top→bottom):**
|
||||||
|
1. **Input row**: `⌕` glyph (accent color) + optional type-filter pill + input field + optional Tab ghost hint
|
||||||
|
2. **Type chips row**: scrollable chip row for all/deploy/svc/pods/secrets/config/sts/crd/namespace/ns. Scrollable horizontally on small screens.
|
||||||
|
3. **Results list** (`max-height: 48vh, overflow: auto`): each row = shape glyph + name + subtext (type · ns/namespace) + CRD badge if applicable + type badge
|
||||||
|
4. **View chips** (expanded on the selected row only): `⌥L logs · ⌥S shell · ⌥D describe · ⌥Y yaml`. Only available views shown — e.g. YAML and Describe are always shown; Logs and Shell only for pods/deployments/daemonsets/statefulsets.
|
||||||
|
5. **Footer**: `↑↓ pick · ⌥L/S/D/Y open views · ⏎ open default · ⇥ filter type · esc done`
|
||||||
|
|
||||||
|
**Fuzzy search algorithm:**
|
||||||
|
- Score = character-match score × 10 + type boost
|
||||||
|
- Type boost: crd=60, deployment=50, statefulset=48, service=46, daemonset=44, ingress=42, secret=24, configmap=22, pvc=18, pod=16
|
||||||
|
- Character matching: sequential character match with bonuses for consecutive matches (+3) and word-boundary starts (+5)
|
||||||
|
- Results capped at 8
|
||||||
|
|
||||||
|
**Tab quick-filter:**
|
||||||
|
- Typing a type alias (`pod`, `svc`, `deploy`, `secret`, `cm`, `sts`…) shows a ghost hint: `⇥ Pods`
|
||||||
|
- Tab locks the filter as a pill; Backspace removes it
|
||||||
|
- Tab also **cycles** through all type filters (recency-ordered, most-recently-opened first)
|
||||||
|
|
||||||
|
**View shortcut keys:**
|
||||||
|
- All use **⌥ (Alt/Option)**. Do NOT use Ctrl — it must stay free for terminal use.
|
||||||
|
- Keys: `Alt+L` = logs, `Alt+S` = shell, `Alt+D` = describe, `Alt+Y` = yaml
|
||||||
|
- Use `event.code` (layout-safe, e.g. `KeyL`) not `event.key` (which varies by OS/keyboard layout)
|
||||||
|
- Multiple views can be opened before closing spotlight — they stack as windows in one krate
|
||||||
|
- On Enter: opens the default view (logs for pods, describe for workloads, yaml for config/secrets/pvcs) and closes
|
||||||
|
|
||||||
|
**Auto-close:** after 500ms of keyboard idle (no more keypresses), spotlight closes automatically and the camera flies to the newly created krate.
|
||||||
|
|
||||||
|
**Search also returns:**
|
||||||
|
- Existing krates (jump to working set, camera flies there)
|
||||||
|
- Namespace results (`ns/payments`) → opens a collection window
|
||||||
|
- Category results (`All Pods`, `All Services`…) → opens a collection window
|
||||||
|
|
||||||
|
### 4. Krate (window group)
|
||||||
|
|
||||||
|
**Purpose:** A named group of detail windows for a single query/object. Lives on the canvas.
|
||||||
|
|
||||||
|
**Visual container:**
|
||||||
|
- Dashed border frame (rounded rect) around all its windows: `border: 1px dashed rgba(color, .3); border-radius: 18px; background: rgba(color, .04)`
|
||||||
|
- Frame auto-fits to the bounding box of its windows + 30px padding
|
||||||
|
- Header label bar above the frame: shows object name, status badge, window count, **drag handle** to move the entire krate, **×** to dismiss
|
||||||
|
|
||||||
|
**Layout of windows inside a krate:**
|
||||||
|
- Windows are tiled in a 2-column grid (col 0 = dx:0, col 1 = dx:412; row height = 416px)
|
||||||
|
- When one window is resized, the others resize proportionally within the grid column/row
|
||||||
|
- Windows never overlap (snap-to-grid on release)
|
||||||
|
|
||||||
|
**Krate interactions:**
|
||||||
|
- **Drag header** = move entire krate
|
||||||
|
- **Double-click header** = collapse/expand (toggle)
|
||||||
|
- **Minimize button** (`—`) in header = collapse to overview card
|
||||||
|
- **Collapsed state**: 230px-wide card showing name, view-letter badges (Y/D/L/S), status dot, window count, ns. **Double-click to re-expand and fly camera to it.**
|
||||||
|
- **Non-overlapping placement:** on mouse release after drag, check all other krate bounding boxes and nudge to avoid overlap. Gentle snap, not strict grid.
|
||||||
|
|
||||||
|
**Camera fly-to on open:** after summon, camera animates to center the krate at `zoom: 0.92`.
|
||||||
|
|
||||||
|
### 5. Detail Window
|
||||||
|
|
||||||
|
**Purpose:** Shows one view (YAML / Describe / Logs / Shell) for one Kubernetes object.
|
||||||
|
|
||||||
|
**Size:** Default 380×362px. Resizable by dragging the bottom-right corner handle (resize one window in a krate → sibling windows resize proportionally in the same column/row).
|
||||||
|
|
||||||
|
**Header (36px):**
|
||||||
|
- Shape glyph (8px, color-coded by object type) + view label (uppercase, accent-colored) + object name + status badge + **×** close
|
||||||
|
- `cursor: grab` — drag to reposition within the canvas
|
||||||
|
- **Double-click header** = zoom window to fill available space (below top bar, above bottom bar, side margins 22px). Press `z` or double-click again to restore.
|
||||||
|
|
||||||
|
**Tab bar:** YAML · Describe · Logs · Shell. Shell tab is disabled (muted, opacity 0.45) for non-exec object types.
|
||||||
|
|
||||||
|
**Content area:** `<pre>` with `overflow: auto; height: 286px; font-family: 'IBM Plex Mono'; font-size: 12px; line-height: 1.65`. Scrollbar styled (8px, rgba(140,165,200,.18)).
|
||||||
|
|
||||||
|
**Keyboard focus:**
|
||||||
|
- **Mouse-over a window → start typing** = focuses that window's content (specifically the shell input if it's a shell view)
|
||||||
|
- Mouse-over detection: track `mouseenter`/`mouseleave` per window, set a `hoveredWindowId` ref
|
||||||
|
- In the global `keydown` capture handler: if `hoveredWindowId` refers to a shell, route the keypress there; if it's any other window, do nothing (let it fall through to canvas shortcuts)
|
||||||
|
|
||||||
|
**Maximize (`z` key or double-click header):**
|
||||||
|
- Resize the window to `width = viewport.width - 44px`, `height = viewport.height - topBarHeight(68px) - bottomBarHeight(66px)`
|
||||||
|
- Set camera so `camX = 22 - krate.wx - window.dx`, `camY = 68 - krate.wy - window.dy`, `zoom = 1`
|
||||||
|
- `z` again or double-click to restore previous size
|
||||||
|
|
||||||
|
**Secret YAML auto-decode:**
|
||||||
|
- When rendering YAML for a Secret object, base64-decode all data values and show plaintext with a `⊙ secret values auto-decoded` banner
|
||||||
|
|
||||||
|
### 6. Shell Window
|
||||||
|
|
||||||
|
**Purpose:** Interactive pseudo-terminal session inside the app.
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- Use **xterm.js** (or equivalent) for the real terminal
|
||||||
|
- Backend: WebSocket → kubectl exec into the pod
|
||||||
|
- Keyboard routing: the window captures keypresses when the mouse is over it (see above). Space must NOT open the spotlight when the cursor is over a shell — instead pass the space character to the terminal.
|
||||||
|
- Scroll wheel over shell: scroll the terminal content, do NOT pan the canvas. Only `Ctrl/⌘+scroll` over a shell should zoom the canvas.
|
||||||
|
|
||||||
|
**Mock in prototype:** Shows static text with a blinking cursor character. The real implementation connects via `kubectl exec -it <pod> -n <ns> -- sh` through the backend.
|
||||||
|
|
||||||
|
### 7. Logs Window
|
||||||
|
|
||||||
|
**Purpose:** Live-tailing container logs.
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- Streams via `kubectl logs --follow` through the backend WebSocket
|
||||||
|
- Auto-scroll to bottom when new lines arrive, unless the user has scrolled up (detect by comparing `scrollTop + clientHeight` vs `scrollHeight`; if not at bottom, stop auto-scroll)
|
||||||
|
- Color-code: ERROR lines in `#ef6f6f`, WARN in `#e8b54a`, INFO in `#b9c6d8`
|
||||||
|
|
||||||
|
### 8. Collection Window
|
||||||
|
|
||||||
|
**Purpose:** Overview of a category (All Pods, All Services…) or namespace. Status-at-a-glance list.
|
||||||
|
|
||||||
|
**Size:** Default 540×480px.
|
||||||
|
|
||||||
|
**Header:**
|
||||||
|
- Title (e.g. "pods · ns/payments"), status summary badges (✓ N / ⚠ N / ✗ N)
|
||||||
|
- **Filter input** (auto-focused on open): `font-family: IBM Plex Mono; font-size: 12px`
|
||||||
|
- Keyboard hint: `↑↓ ⌥L/S/D/Y` (always visible, muted)
|
||||||
|
- **List / Tree** toggle buttons
|
||||||
|
|
||||||
|
**List mode:**
|
||||||
|
- Each row: shape glyph + name + relation tag (muted, for tree mode) + health metric + type badge + view buttons (⌥L/S/D/Y) + status dot
|
||||||
|
- Selected row: `background: accentDim; border-left: 2px solid accent`
|
||||||
|
- Hover row: `background: rgba(255,255,255,.04)`
|
||||||
|
- Sorted: degraded/failed first, then pending, then ready; alpha within each group
|
||||||
|
|
||||||
|
**Tree mode (k9s xray):**
|
||||||
|
- Each workload expands to show its pods; each pod/workload expands to show its configMaps, secrets, PVCs
|
||||||
|
- Services/ingresses expand to show their targets
|
||||||
|
- Relation tags shown: `pod`, `configMap`, `secret`, `volume`, `selects`, `routes`, `used by`
|
||||||
|
- Indent per level: `20px * depth`; connector shown as an L-shaped border element
|
||||||
|
|
||||||
|
**Filter behavior in collection window:**
|
||||||
|
- Typing filters immediately (no mode switching)
|
||||||
|
- `↑/↓` moves selection highlight
|
||||||
|
- `⌥L/S/D/Y` opens that view for the selected row (uses `event.code`, Alt required)
|
||||||
|
- `Enter` opens default view for selected row
|
||||||
|
- `Escape` blurs the input
|
||||||
|
- Mouse-over the collection window + start typing = focuses the filter input and types there (same hover routing as shell windows, but for the filter input)
|
||||||
|
- In tree mode, filtering keeps matching nodes **plus all their ancestors** so the hierarchy stays readable
|
||||||
|
|
||||||
|
### 9. Admin Drawer
|
||||||
|
|
||||||
|
**Purpose:** Right-side panel showing all users connected to the yard.
|
||||||
|
|
||||||
|
**Trigger:** `◉ admin` button in top bar (toggle).
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
- Right-side slide-in panel: `width: 380px; top: 0; right: 0; bottom: 0; position: absolute; z-index: 56`
|
||||||
|
- Backdrop: `rgba(7,9,13,.4)`, click to close
|
||||||
|
- Background: `rgba(13,17,24,.98); border-left: 1px solid rgba(140,165,200,.2)`
|
||||||
|
|
||||||
|
**Content per user:**
|
||||||
|
- Avatar circle + name + status (active · namespace / idle Nm)
|
||||||
|
- Current search query + "typing…" pulsing indicator if they have spotlight open
|
||||||
|
- Krate count
|
||||||
|
- Each open krate as a clickable row: name + view-letter badges + status dot
|
||||||
|
|
||||||
|
**Admin can spectate:** clicking a krate row in another user's card (or in "You") closes the admin panel and flies the camera to that krate.
|
||||||
|
|
||||||
|
**Your own row** shows real-time state — your current spotlight query and actual open krates.
|
||||||
|
|
||||||
|
### 10. Minimap
|
||||||
|
|
||||||
|
**Purpose:** Overview of all krates' positions on the canvas.
|
||||||
|
|
||||||
|
**Position:** `right: 18px; bottom: 64px`
|
||||||
|
**Size:** `180×120px`, with a viewport indicator rectangle showing the current view frustum.
|
||||||
|
|
||||||
|
**Interaction:** Click anywhere on the minimap to fly the camera to that world position.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interactions & Behavior Summary
|
||||||
|
|
||||||
|
| Interaction | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| Click empty canvas | Open spotlight |
|
||||||
|
| Type any key (not in input) | Open spotlight pre-seeded with that key |
|
||||||
|
| `/` key | Open spotlight empty |
|
||||||
|
| `Esc` | Close spotlight / close spotlight and place krate |
|
||||||
|
| Scroll wheel | Pan canvas |
|
||||||
|
| Ctrl/⌘ + scroll | Zoom canvas |
|
||||||
|
| Space + drag | Pan canvas (even over windows/shells) |
|
||||||
|
| `z` over a window | Maximize window to fill available space |
|
||||||
|
| `z` again | Restore window size |
|
||||||
|
| Double-click window header | Maximize / restore |
|
||||||
|
| Double-click krate header | Collapse / expand krate |
|
||||||
|
| Double-click collapsed krate card | Expand and fly camera to it |
|
||||||
|
| Mouse-over window + type | Focus shell / collection filter |
|
||||||
|
| ⌥L / ⌥S / ⌥D / ⌥Y | Open view for selected result (spotlight or collection) |
|
||||||
|
| Tab in spotlight | Apply type filter / cycle type filters |
|
||||||
|
| Backspace in spotlight (empty query, filter active) | Clear type filter |
|
||||||
|
| ↑↓ in spotlight | Navigate results |
|
||||||
|
| ⌥+key in spotlight | Open view for selected result |
|
||||||
|
| ↑↓ in collection filter | Move row selection |
|
||||||
|
| ⌥+key in collection | Open view for selected row |
|
||||||
|
| Drag window header | Move window |
|
||||||
|
| Drag krate header | Move entire krate |
|
||||||
|
| Krate drag release | Nudge to avoid overlap with other krates |
|
||||||
|
| Resize window handle | Resize; siblings resize proportionally |
|
||||||
|
| Minimap click | Fly camera to position |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Canvas state
|
||||||
|
```typescript
|
||||||
|
interface CanvasState {
|
||||||
|
camX: number; // world-to-screen X offset
|
||||||
|
camY: number; // world-to-screen Y offset
|
||||||
|
zoom: number; // 0.16 – 2.2; default 0.92
|
||||||
|
flying: boolean; // true during animated camera transitions
|
||||||
|
dragging: boolean; // true during pan drag
|
||||||
|
collapsed: boolean; // LOD: true when zoom < 0.4
|
||||||
|
spacePanning: boolean; // true while space is held
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Krate state
|
||||||
|
```typescript
|
||||||
|
interface Krate {
|
||||||
|
id: string;
|
||||||
|
objId: string | null; // null for collection krates
|
||||||
|
collScope?: { kind: 'namespace' | 'category', value: string };
|
||||||
|
label: string;
|
||||||
|
status: string;
|
||||||
|
color: string; // hex, from namespace palette
|
||||||
|
wx: number; wy: number; // world position
|
||||||
|
minimized: boolean;
|
||||||
|
windows: Window[];
|
||||||
|
seq: number; // for generating window IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
wid: string;
|
||||||
|
kind: 'detail' | 'collection';
|
||||||
|
tab: 'logs' | 'shell' | 'describe' | 'yaml';
|
||||||
|
dx: number; dy: number; // offset within krate
|
||||||
|
w: number; h: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spotlight state
|
||||||
|
```typescript
|
||||||
|
interface SpotlightState {
|
||||||
|
open: boolean;
|
||||||
|
query: string;
|
||||||
|
filterType: string | null;
|
||||||
|
sel: number; // selected result index
|
||||||
|
navigated: boolean; // true after ↑/↓ press
|
||||||
|
session: { // tracks views opened before closing
|
||||||
|
kid: string;
|
||||||
|
objId: string;
|
||||||
|
views: string[];
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collection window state (per-window ID)
|
||||||
|
```typescript
|
||||||
|
interface CollectionState {
|
||||||
|
[wid: string]: {
|
||||||
|
search: string;
|
||||||
|
view: 'list' | 'tree';
|
||||||
|
sel: number; // selected row index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend / API Requirements
|
||||||
|
|
||||||
|
The prototype uses **mock data**. The real app needs:
|
||||||
|
|
||||||
|
### Kubernetes data
|
||||||
|
- **kubectl proxy** or a dedicated Go/Rust aggregation service
|
||||||
|
- Watch API for live updates: `GET /api/v1/namespaces/{ns}/pods?watch=true` etc.
|
||||||
|
- Resources needed: Namespaces, Deployments, StatefulSets, DaemonSets, Services, Ingresses, ConfigMaps, Secrets, PVCs, Events, and CRDs
|
||||||
|
- Secret values should be base64-decoded server-side (or client-side) before display
|
||||||
|
|
||||||
|
### Real-time shell
|
||||||
|
- WebSocket → `kubectl exec -it` stream
|
||||||
|
- Bidirectional: keypresses → stdin, stdout/stderr → display
|
||||||
|
|
||||||
|
### Real-time logs
|
||||||
|
- WebSocket → `kubectl logs --follow`
|
||||||
|
- Stream new lines; handle reconnection
|
||||||
|
|
||||||
|
### Multi-user sync (the Yard)
|
||||||
|
- **CRDTs or operational transforms** for shared krate positions, krate existence, window layout
|
||||||
|
- Recommended: **Yjs** (CRDT library) over WebSocket (y-websocket)
|
||||||
|
- Each user's cursor/active query broadcast as ephemeral state (Yjs awareness)
|
||||||
|
- Admin view reads from the awareness map
|
||||||
|
|
||||||
|
### Recommended backend stack
|
||||||
|
- **Go** service wrapping the Kubernetes client-go library
|
||||||
|
- WebSocket multiplexer for shell, logs, watch streams
|
||||||
|
- **Redis** or **in-memory** for session/presence if single-node
|
||||||
|
- For multi-node: use Yjs with a persistent provider (y-leveldb or y-mongodb)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Tokens
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
```
|
||||||
|
Background: #0b0e13
|
||||||
|
World background: #0b0e13
|
||||||
|
Surface: rgba(14,18,25,.97)
|
||||||
|
Surface elevated: rgba(20,26,36,.7)
|
||||||
|
Border default: rgba(140,165,200,.2)
|
||||||
|
Border subtle: rgba(140,165,200,.14)
|
||||||
|
|
||||||
|
Accent (cyan): #4dd6e8
|
||||||
|
Accent (violet): #a98cff
|
||||||
|
Accent (amber): #f5b454
|
||||||
|
|
||||||
|
Text primary: #e6edf6
|
||||||
|
Text secondary: #c7d2e0
|
||||||
|
Text muted: #9fb0c8
|
||||||
|
Text faint: #7e8aa2
|
||||||
|
Text dimmed: #6b7890
|
||||||
|
|
||||||
|
Status OK: #4ad07a
|
||||||
|
Status warn: #e8b54a
|
||||||
|
Status error: #ef6f6f
|
||||||
|
|
||||||
|
Object colors:
|
||||||
|
Deployment: #6fb1ff
|
||||||
|
StatefulSet: #b89cff
|
||||||
|
Service: #5fd0c0
|
||||||
|
DaemonSet: #ffb27a
|
||||||
|
Ingress: #e58fb0
|
||||||
|
CRD: #ffd479
|
||||||
|
Pod: #8aa0bd
|
||||||
|
ConfigMap: #7fb39c
|
||||||
|
Secret: #c79bd6
|
||||||
|
PVC: #9aa7c7
|
||||||
|
|
||||||
|
Namespace palettes:
|
||||||
|
payments: #6fb1ff
|
||||||
|
platform: #5fd0c0
|
||||||
|
storefront: #e58fb0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shape language (CSS clip-path per object type)
|
||||||
|
```
|
||||||
|
Deployment: border-radius: 50% (circle)
|
||||||
|
StatefulSet: polygon(50% 0, 100% 100%, 0 100%) (triangle)
|
||||||
|
Service: polygon(50% 0, 100% 50%, 50% 100%, 0 50%) (diamond)
|
||||||
|
DaemonSet: ring (transparent fill, colored border)
|
||||||
|
Ingress: polygon(50% 0, 100% 100%, 0 100%) (triangle, same as STS)
|
||||||
|
CRD: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%) (hexagon)
|
||||||
|
Pod: circle
|
||||||
|
ConfigMap: square (border-radius: 3px)
|
||||||
|
Secret: hexagon (same clip-path as CRD, smaller)
|
||||||
|
PVC: square
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
```
|
||||||
|
UI font: IBM Plex Sans, 400/500/600
|
||||||
|
Mono font: IBM Plex Mono, 400/500
|
||||||
|
|
||||||
|
Sizes used:
|
||||||
|
9px — label/badge uppercase legends
|
||||||
|
10px — tiny metadata, status labels
|
||||||
|
11px — chips, hints, secondary metadata
|
||||||
|
12px — mono body text in windows/collections
|
||||||
|
13px — nav bar labels, search result sub-text
|
||||||
|
14px — collection titles, admin panel
|
||||||
|
17px — spotlight search input prefix glyph
|
||||||
|
18px — spotlight search input
|
||||||
|
25px — empty state headline
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
```
|
||||||
|
Window default size: 380 × 362px
|
||||||
|
Collection default: 540 × 480px
|
||||||
|
Window grid col gap: 412px (column pitch)
|
||||||
|
Window grid row gap: 416px (row pitch)
|
||||||
|
Krate frame padding: 30px around bounding box
|
||||||
|
Top bar height: ~56px (inset: 68px with padding)
|
||||||
|
Bottom bar height: ~40px (inset: 66px with padding)
|
||||||
|
Minimap: 180 × 120px
|
||||||
|
Sidebar margin: 18px
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animations
|
||||||
|
```
|
||||||
|
Camera fly: transform .52s cubic-bezier(.22,.8,.28,1)
|
||||||
|
Spotlight open: opacity + translateY(-8px→0) .16s cubic-bezier(.2,.8,.3,1)
|
||||||
|
Window appear: opacity + scale(.97→1) .16s ease
|
||||||
|
Fade backdrop: opacity .12s ease
|
||||||
|
Krate logo float: translateY 0→-7px→0, 4s ease-in-out infinite
|
||||||
|
Synced dot blink: opacity 50% at 50%, 1.6s ease-in-out infinite
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keyboard Shortcut Reference
|
||||||
|
|
||||||
|
All shortcuts that act on objects use **⌥ (Alt/Option)** — never Ctrl (reserved for terminal), never bare letters (reserved for typing in search/filter).
|
||||||
|
|
||||||
|
| Key | Context | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| Any printable key | Canvas focus | Open spotlight pre-seeded |
|
||||||
|
| `/` | Canvas focus | Open spotlight empty |
|
||||||
|
| `Esc` | Spotlight open | Close / finish + place krate |
|
||||||
|
| `↑/↓` | Spotlight | Navigate results |
|
||||||
|
| `Tab` | Spotlight | Apply type filter / cycle filters |
|
||||||
|
| `⌥L/S/D/Y` | Spotlight (result selected) | Open that view |
|
||||||
|
| `Enter` | Spotlight | Open default view + close |
|
||||||
|
| `↑/↓` | Collection filter focused | Move row selection |
|
||||||
|
| `⌥L/S/D/Y` | Collection filter focused | Open that view for selected row |
|
||||||
|
| `Enter` | Collection filter focused | Open default view for selected row |
|
||||||
|
| `Space` + drag | Canvas | Pan (even over windows/shells) |
|
||||||
|
| `Ctrl/⌘` + scroll | Anywhere | Zoom canvas |
|
||||||
|
| `z` | Mouse over window | Maximize / restore window |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Reference
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `Krates.dc.html` | Full interactive prototype — the primary design reference. Open in any modern browser. |
|
||||||
|
| `README.md` | This document |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes & Gotchas
|
||||||
|
|
||||||
|
1. **Space pan over shells:** The trickiest keyboard interaction. The space key must pan the canvas even when a `<textarea>` or xterm.js instance has focus. Implement with a `document.addEventListener('keydown', handler, { capture: true })` that checks if space is pressed during a drag gesture, sets a flag, and calls `e.preventDefault()` on the focused element. Do NOT use `e.stopPropagation()` — you still need the event to reach other handlers.
|
||||||
|
|
||||||
|
2. **Ctrl/⌘+scroll over windows:** Similarly, `wheel` events on window content areas will be captured by the scroll container. Use `{ capture: true }` on the canvas wheel handler to intercept before the window, check for `e.ctrlKey || e.metaKey`, and only zoom then. Otherwise let the event scroll the window content.
|
||||||
|
|
||||||
|
3. **LOD hysteresis:** Use separate thresholds for collapse (0.4) and expand (0.5) to prevent jitter. Store `collapsed` as boolean in state, not derived from `zoom` on every render.
|
||||||
|
|
||||||
|
4. **Non-overlapping krate placement:** On each new krate creation, try the computed default position, then nudge by fixed increments until no bounding-box overlap exists with any existing krate. On drag release, do the same nudge.
|
||||||
|
|
||||||
|
5. **Fuzzy search speed:** The prototype scores all objects on every keystroke. For a real cluster with thousands of objects, debounce by 16ms (one animation frame) or run scoring in a Web Worker. The scoring algorithm is simple enough that even 10,000 objects should score in <5ms.
|
||||||
|
|
||||||
|
6. **Collection filter auto-focus:** When a collection krate is created, `focus()` its filter `<input>` in the next tick. Use a ref stored during render and a flag cleared in `componentDidUpdate` / `useEffect`.
|
||||||
|
|
||||||
|
7. **Secret decoding:** The prototype shows decoded secrets inline with a banner. In the real app, ensure that decoded secrets are handled carefully — consider requiring explicit user action to reveal, and never log them.
|
||||||
|
|
||||||
|
8. **CRDT sync scope:** Sync krate positions, existence, and window layout via CRDT. Do NOT sync window content (logs, YAML, shell) — each client streams independently from the k8s API. Only sync the *structure* of the workspace.
|
||||||
|
|
||||||
|
9. **Resize handles:** The prototype does not implement drag-resize yet. When implementing, a resize in one column/row should proportionally adjust sibling windows. Store a `colWidths` and `rowHeights` array per krate, not absolute `dx/dy` per window.
|
||||||
|
|
||||||
|
10. **Admin presence data:** The prototype uses hardcoded mock users. In the real app, use Yjs awareness or a presence channel: each client publishes `{ userId, name, color, query, krateIds[] }` as ephemeral state; the admin panel subscribes and renders it reactively.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Tech Stack
|
||||||
|
|
||||||
|
```
|
||||||
|
Frontend: React 18 + TypeScript
|
||||||
|
Styling: CSS-in-JS (styled-components or vanilla-extract) or Tailwind
|
||||||
|
Terminal: xterm.js
|
||||||
|
Real-time: Yjs + y-websocket (CRDT collaboration)
|
||||||
|
Backend: Go + kubernetes/client-go + gorilla/websocket
|
||||||
|
Fonts: IBM Plex Sans + IBM Plex Mono (Google Fonts or self-hosted)
|
||||||
|
Build: Vite
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backlog (Not in Prototype)
|
||||||
|
|
||||||
|
- **Tile layout presets** per krate: "logs hero" (one large log window, data tiles around it), "split", "grid". Shortcut on krate header.
|
||||||
|
- **Port-forwarding window**: dedicated view type alongside YAML/Logs/Shell/Describe
|
||||||
|
- **Events window**: `kubectl get events --watch` for a namespace or object
|
||||||
|
- **Diff view**: compare YAML between two versions or two objects
|
||||||
|
- **Multi-cluster support**: multiple yards in one app, switchable from the top bar
|
||||||
|
|||||||
188
design/admin-drawer.md
Normal file
188
design/admin-drawer.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Admin Drawer Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The admin drawer is a right-side panel that displays all users connected to the current yard (shared cluster workspace). It enables administrators to see real-time user activity and spectate on other users' workspaces.
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
### Activation
|
||||||
|
- Button: `◉ admin` in top bar
|
||||||
|
- Toggle: Click to open/close
|
||||||
|
- Close on: clicking backdrop, pressing Esc
|
||||||
|
|
||||||
|
### Modal Overlay
|
||||||
|
- Backdrop: `rgba(7,9,13,.4)`
|
||||||
|
- Click backdrop: Close drawer
|
||||||
|
- Click outside: Close drawer
|
||||||
|
|
||||||
|
## Drawer Layout
|
||||||
|
|
||||||
|
### Container
|
||||||
|
- Width: 380px
|
||||||
|
- Position: `absolute; right: 0; top: 0; bottom: 0; z-index: 56`
|
||||||
|
- Background: `rgba(13,17,24,.98)`
|
||||||
|
- Border: `border-left: 1px solid rgba(140,165,200,.2)`
|
||||||
|
- Animation: Slide-in from right
|
||||||
|
|
||||||
|
### Content Structure
|
||||||
|
- Header (fixed, sticky)
|
||||||
|
- Scrollable user list
|
||||||
|
- Footer (optional, for actions)
|
||||||
|
|
||||||
|
## User Card Structure
|
||||||
|
|
||||||
|
For each connected user (including self):
|
||||||
|
|
||||||
|
### Avatar Section
|
||||||
|
- Circle: 40px直径
|
||||||
|
- Background: User color from palette
|
||||||
|
- Content:
|
||||||
|
- For self: `★` (star)
|
||||||
|
- For others: 2-letter initials
|
||||||
|
- Hover: Show full name + status tooltip
|
||||||
|
|
||||||
|
### Identity
|
||||||
|
- Name (bold)
|
||||||
|
- Status:
|
||||||
|
- Active: `active · ns/payments`
|
||||||
|
- Idle: `idle 5m` (or `idle Nm`)
|
||||||
|
|
||||||
|
### Activity Info
|
||||||
|
- Current spotlight query (if open)
|
||||||
|
- Display: `🔍 pods in payments`
|
||||||
|
- "typing…" pulsing indicator (if currently typing)
|
||||||
|
- Krate count: `N krates open`
|
||||||
|
|
||||||
|
### Open Krate List
|
||||||
|
Collapsible section showing each active krate:
|
||||||
|
- Clickable row
|
||||||
|
- Content:
|
||||||
|
- Krate name
|
||||||
|
- View-letter badges: `Y`, `D`, `L`, `S`
|
||||||
|
- Status dot (health indicator)
|
||||||
|
- **Spectate**: Click any krate row to:
|
||||||
|
- Close admin panel
|
||||||
|
- Fly camera to that krate
|
||||||
|
- Zoom to center the krate at `zoom: 0.92`
|
||||||
|
|
||||||
|
### Self User Special Case
|
||||||
|
- Real-time state updates
|
||||||
|
- Shows actual spotlight query
|
||||||
|
- Lists user's actual open krates
|
||||||
|
- Updates as user interacts
|
||||||
|
|
||||||
|
## Real-Time Updates
|
||||||
|
|
||||||
|
### Presence Protocol
|
||||||
|
Each client publishes to awareness channel:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
userId: string,
|
||||||
|
name: string,
|
||||||
|
color: string,
|
||||||
|
lastActive: timestamp,
|
||||||
|
query: string | null, // spotlight query
|
||||||
|
krateIds: string[], // list of open krate IDs
|
||||||
|
cursor: { x, y } | null // optional cursor position
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Frequency
|
||||||
|
- On connection/disconnection: immediate
|
||||||
|
- On spotlight open/close: immediate
|
||||||
|
- On krate create/delete: immediate
|
||||||
|
- Periodic heartbeat: every 5 seconds
|
||||||
|
|
||||||
|
## Keyboard Navigation
|
||||||
|
|
||||||
|
| Key | Context | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| Esc | Admin drawer open | Close drawer |
|
||||||
|
| ↑ / ↓ | Drawer focused | Navigate user cards |
|
||||||
|
| Click user card | User list | Expand/collapse krate list |
|
||||||
|
| Click krate row | Krate list | Spectate (fly to krate) |
|
||||||
|
| Click backdrop | Anywhere | Close drawer |
|
||||||
|
|
||||||
|
## User Sorting
|
||||||
|
- Self user: Always first
|
||||||
|
- Active users: Ordered by last active (most recent first)
|
||||||
|
- Idle users: Ordered by last active
|
||||||
|
- Collapsed by default: Minimize height
|
||||||
|
|
||||||
|
## Visual Feedback
|
||||||
|
|
||||||
|
### Active/Idle Indicators
|
||||||
|
- Active: Solid color avatar, no dimming
|
||||||
|
- Idle: Dimmed avatar (80% opacity), `idle Nm` label
|
||||||
|
|
||||||
|
### Typing Indicator
|
||||||
|
- Text: `typing…`
|
||||||
|
- Color: Muted accent
|
||||||
|
- Animation: Pulsing opacity or small bounce
|
||||||
|
- Show when user has spotlight open AND hasn't typed in <1s
|
||||||
|
|
||||||
|
### Status Dots
|
||||||
|
- Krate status: Green/amber/red dot (matching window content)
|
||||||
|
- Pod status dot: Already in krate view (reuse same color logic)
|
||||||
|
|
||||||
|
## Admin Features (Optional Future)
|
||||||
|
|
||||||
|
### Control Actions
|
||||||
|
- Notify user: Send notification to specific user
|
||||||
|
- Suspend: Temporarily disconnect user (for debugging)
|
||||||
|
- View logs: See admin-level logs per user
|
||||||
|
|
||||||
|
### Spectate Controls
|
||||||
|
- Step forward/backward: Next/previous krate
|
||||||
|
- Cycle through users: Left/right arrow
|
||||||
|
- Lock view: Prevent camera from moving (spectator mode)
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Drawer State
|
||||||
|
```typescript
|
||||||
|
interface AdminState {
|
||||||
|
open: boolean;
|
||||||
|
users: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
self: boolean;
|
||||||
|
status: 'active' | 'idle';
|
||||||
|
idleMinutes?: number;
|
||||||
|
query?: string;
|
||||||
|
typing: boolean;
|
||||||
|
krateIds: string[];
|
||||||
|
}[];
|
||||||
|
focusedUserId: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User State
|
||||||
|
```typescript
|
||||||
|
interface UserPresence {
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
lastActive: number; // timestamp
|
||||||
|
lastTyping: number; // timestamp
|
||||||
|
spotlightQuery: string | null;
|
||||||
|
openKrates: KratePresence[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KratePresence {
|
||||||
|
krateId: string;
|
||||||
|
label: string;
|
||||||
|
viewCount: number;
|
||||||
|
views: string[]; // ['Y', 'D', 'L', 'S']
|
||||||
|
status: string; // from first window
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
1. **Self-updates**: User's own card must show real-time state, not cached data
|
||||||
|
2. **Focus management**: Keep focus on drawer when open, trap Tab cycle
|
||||||
|
3. **Idle detection**: 60 seconds of no activity = idle
|
||||||
|
4. **Typing indicator**: Debounce by 1000ms after last keypress
|
||||||
|
5. **Spectate behavior**: Close admin panel first, THEN fly camera
|
||||||
|
6. **Roster overflow**: Handle >10 users gracefully (scroll, paginate, or truncate)
|
||||||
312
design/architecture.md
Normal file
312
design/architecture.md
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
# Top-Level Application Architecture
|
||||||
|
|
||||||
|
## Tech Stack Recommendations
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Framework**: React 18 + TypeScript
|
||||||
|
- **Styling**: CSS-in-JS (styled-components or vanilla-extract) or Tailwind CSS
|
||||||
|
- **Terminal**: xterm.js
|
||||||
|
- **Real-Time**: Yjs + y-websocket (CRDT collaboration)
|
||||||
|
- **Build Tool**: Vite
|
||||||
|
- **Fonts**: IBM Plex Sans + IBM Plex Mono (Google Fonts or self-hosted)
|
||||||
|
- **State Management**: Zustand or React Context (keep it simple)
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Language**: Go
|
||||||
|
- **Kubernetes Client**: kubernetes/client-go
|
||||||
|
- **WebSockets**: gorilla/websocket
|
||||||
|
- **CRDT Backend**: Redis (single-node) or y-leveldb/y-mongodb (multi-node)
|
||||||
|
- **Build/Deployment**: Docker, Kubernetes
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
krates/
|
||||||
|
├── client/ # Frontend application
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React components
|
||||||
|
│ │ │ ├── canvas/ # Canvas and world layer
|
||||||
|
│ │ │ │ ├── WorldLayer.tsx
|
||||||
|
│ │ │ │ ├── GridOverlay.tsx
|
||||||
|
│ │ │ │ └── Minimap.tsx
|
||||||
|
│ │ │ ├── krate/ # Krate and windows
|
||||||
|
│ │ │ │ ├── Krate.tsx
|
||||||
|
│ │ │ │ ├── KrateHeader.tsx
|
||||||
|
│ │ │ │ ├── DetailWindow.tsx
|
||||||
|
│ │ │ │ └── CollectionWindow.tsx
|
||||||
|
│ │ │ ├── Spotlight.tsx
|
||||||
|
│ │ │ ├── TopBar.tsx
|
||||||
|
│ │ │ ├── AdminDrawer.tsx
|
||||||
|
│ │ │ ├── ShellTerminal.tsx # xterm.js wrapper
|
||||||
|
│ │ │ └── LogsViewer.tsx
|
||||||
|
│ │ ├── hooks/ # Custom React hooks
|
||||||
|
│ │ │ ├── useCanvas.ts
|
||||||
|
│ │ │ ├── useSpotlight.ts
|
||||||
|
│ │ │ ├── useWebSocket.ts
|
||||||
|
│ │ │ └── useKubernetes.ts
|
||||||
|
│ │ ├── state/ # State management
|
||||||
|
│ │ │ ├── canvasStore.ts
|
||||||
|
│ │ │ ├── krateStore.ts
|
||||||
|
│ │ │ └── spotlightStore.ts
|
||||||
|
│ │ ├── utils/
|
||||||
|
│ │ │ ├── fuzzy.ts # Fuzzy search algorithm
|
||||||
|
│ │ │ ├── crdt.ts # Yjs setup
|
||||||
|
│ │ │ ├── keyboard.ts # Keyboard handling
|
||||||
|
│ │ │ └── math.ts # Canvas math helpers
|
||||||
|
│ │ ├── App.tsx
|
||||||
|
│ │ └── main.tsx
|
||||||
|
│ ├── public/ # Static assets
|
||||||
|
│ └── package.json
|
||||||
|
│
|
||||||
|
├── server/ # Backend service
|
||||||
|
│ ├── cmd/
|
||||||
|
│ │ └── server/
|
||||||
|
│ │ └── main.go
|
||||||
|
│ ├── internal/
|
||||||
|
│ │ ├── api/ # HTTP endpoints
|
||||||
|
│ │ │ ├── handlers/
|
||||||
|
│ │ │ └── routes.go
|
||||||
|
│ │ ├── ws/ # WebSocket handlers
|
||||||
|
│ │ │ ├── shell.go
|
||||||
|
│ │ │ ├── logs.go
|
||||||
|
│ │ │ └── watch.go
|
||||||
|
│ │ ├── k8s/ # Kubernetes client wrappers
|
||||||
|
│ │ │ ├── client.go
|
||||||
|
│ │ │ ├── resources.go
|
||||||
|
│ │ │ └── watch.go
|
||||||
|
│ │ ├── crdt/ # Yjs integration
|
||||||
|
│ │ │ └── provider.go
|
||||||
|
│ │ └── auth/ # Authentication/authorization
|
||||||
|
│ │ └── token.go
|
||||||
|
│ ├── pkg/ # Reusable packages
|
||||||
|
│ └── go.mod
|
||||||
|
│
|
||||||
|
├── design/ # This folder
|
||||||
|
│ ├── canvas.md
|
||||||
|
│ ├── spotlight.md
|
||||||
|
│ ├── krate.md
|
||||||
|
│ ├── detail-window.md
|
||||||
|
│ ├── top-bar.md
|
||||||
|
│ ├── admin-drawer.md
|
||||||
|
│ ├── minimap.md
|
||||||
|
│ ├── backend.md
|
||||||
|
│ └── architecture.md
|
||||||
|
│
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## State Management Strategy
|
||||||
|
|
||||||
|
### Global State (Zustand)
|
||||||
|
```typescript
|
||||||
|
// canvasStore.ts
|
||||||
|
interface CanvasState {
|
||||||
|
camX: number;
|
||||||
|
camY: number;
|
||||||
|
zoom: number;
|
||||||
|
flying: boolean;
|
||||||
|
dragging: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
spacePanning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// krateStore.ts
|
||||||
|
interface KrateStore {
|
||||||
|
krates: Map<string, Krate>;
|
||||||
|
selectedKrateId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// spotlightStore.ts
|
||||||
|
interface SpotlightStore {
|
||||||
|
open: boolean;
|
||||||
|
query: string;
|
||||||
|
filterType: string | null;
|
||||||
|
sel: number;
|
||||||
|
navigated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// userStore.ts
|
||||||
|
interface UserStore {
|
||||||
|
currentUser: User;
|
||||||
|
presence: Map<string, UserPresence>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collection State (Per-Window)
|
||||||
|
```typescript
|
||||||
|
// stored in krate.windows[].state or separate map
|
||||||
|
interface CollectionState {
|
||||||
|
search: string;
|
||||||
|
view: 'list' | 'tree';
|
||||||
|
sel: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## WebSocket Architecture
|
||||||
|
|
||||||
|
### Connection Setup
|
||||||
|
```
|
||||||
|
1. Connect to /ws/sync (CRDT + awareness)
|
||||||
|
2. Connect to /ws/watch (resource updates)
|
||||||
|
3. On-demand connections:
|
||||||
|
- /ws/shell (when shell window opened)
|
||||||
|
- /ws/logs (when logs window opened)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Format
|
||||||
|
```
|
||||||
|
// CRDT sync
|
||||||
|
{
|
||||||
|
type: 'sync',
|
||||||
|
payload: Uint8Array // Yjs update encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Awareness presence
|
||||||
|
{
|
||||||
|
type: 'awareness',
|
||||||
|
updates: UserPresence[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource watch
|
||||||
|
{
|
||||||
|
type: 'resource',
|
||||||
|
action: 'add' | 'update' | 'delete',
|
||||||
|
kind: string, // 'Pod', 'Deployment', etc.
|
||||||
|
resource: K8sObject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shell output
|
||||||
|
{
|
||||||
|
type: 'shell:output',
|
||||||
|
data: string // stdout/stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shell input
|
||||||
|
{
|
||||||
|
type: 'shell:input',
|
||||||
|
data: string // keypresses, etc.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build & Deployment
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```
|
||||||
|
# Frontend
|
||||||
|
cd client
|
||||||
|
npm install
|
||||||
|
npm run dev # Vite dev server
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
cd server
|
||||||
|
go run cmd/server/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
```
|
||||||
|
# Backend
|
||||||
|
cd server
|
||||||
|
go build -o krates-server
|
||||||
|
./krates-server --config config.yaml
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd client
|
||||||
|
npm run build
|
||||||
|
# Serve static files with nginx or similar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
```dockerfile
|
||||||
|
# Backend Dockerfile
|
||||||
|
FROM golang:1.21-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN go build -o /krates ./cmd/server
|
||||||
|
CMD ["/krates"]
|
||||||
|
|
||||||
|
# Frontend Dockerfile
|
||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
CMD ["npm", "start"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
1. **Virtualization**: Don't render off-screen krates if world is large
|
||||||
|
2. **Debounce**: 16ms debouncing on all frequent events
|
||||||
|
3. **Web Workers**: Fuzzy search in worker for large datasets
|
||||||
|
4. **Memoization**: Reuse expensive calculations (transforms, layouts)
|
||||||
|
5. **CSS optimization**: Use transforms, avoid layout thrashing
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
1. **Caching**: Cache static resource lists (with watch for updates)
|
||||||
|
2. **Connection pooling**: Reuse k8s API connections
|
||||||
|
3. **Rate limiting**: Per-user limits on WebSocket connections
|
||||||
|
4. **Compression**: WebSocket message compression
|
||||||
|
5. **Batching**: Batch resource updates when possible
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Fuzzy search algorithm
|
||||||
|
- Coordinate transformations
|
||||||
|
- CRDT conflict resolution
|
||||||
|
- WebSocket message formatting
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Kubernetes API integration
|
||||||
|
- WebSocket connection lifecycle
|
||||||
|
- Multi-user presence sync
|
||||||
|
|
||||||
|
### E2E Tests
|
||||||
|
- User flows: search → spotlight → krate creation
|
||||||
|
- Multi-user: user A creates krate, user B sees it
|
||||||
|
- Drag/drop and resize interactions
|
||||||
|
|
||||||
|
## Security Checklist
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- [ ] JWT or session-based auth on all WebSocket connections
|
||||||
|
- [ ] Token validation on every message
|
||||||
|
- [ ] Expire inactive sessions
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
- [ ] Namespace-level RBAC enforcement
|
||||||
|
- [ ] Block access to unauthorized resources
|
||||||
|
- [ ] Audit logging
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
- [ ] Never log sensitive data (especially decoded secrets)
|
||||||
|
- [ ] Sanitize user inputs
|
||||||
|
- [ ] Escape HTML in text display
|
||||||
|
- [ ] CSP headers on frontend
|
||||||
|
|
||||||
|
### Network
|
||||||
|
- [ ] TLS/HTTPS for all connections
|
||||||
|
- [ ] WSS for WebSocket connections
|
||||||
|
- [ ] Origin validation on WebSocket upgrade
|
||||||
|
|
||||||
|
## Future Enhancements (Backlog)
|
||||||
|
|
||||||
|
### Already Identified
|
||||||
|
- Tile layout presets (logs hero, split, grid)
|
||||||
|
- Port-forwarding window
|
||||||
|
- Events window
|
||||||
|
- Diff view (compare YAMLs)
|
||||||
|
- Multi-cluster support
|
||||||
|
|
||||||
|
### Additional Ideas
|
||||||
|
- Keyboard shortcuts customization
|
||||||
|
- Workspace presets (save/restore layouts)
|
||||||
|
- Annotations/markers on canvas
|
||||||
|
- Shared annotations (multi-user notes)
|
||||||
|
- History/undo stack
|
||||||
|
- Keyboard macro recording
|
||||||
|
- Export workspace (screenshots, layouts)
|
||||||
264
design/backend.md
Normal file
264
design/backend.md
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
# Backend & Real-Time Sync Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The backend service aggregates Kubernetes API data and provides real-time WebSocket connections for shell, logs, and multi-user collaboration.
|
||||||
|
|
||||||
|
## Backend Architecture
|
||||||
|
|
||||||
|
### Recommended Stack
|
||||||
|
- **Language**: Go (for Kubernetes client integration)
|
||||||
|
- **Kubernetes Client**: kubernetes/client-go
|
||||||
|
- **WebSockets**: gorilla/websocket
|
||||||
|
- **CRDT Layer**: Yjs + y-websocket
|
||||||
|
- **Storage**: Redis or in-memory (single-node), y-leveldb/y-mongodb (multi-node)
|
||||||
|
|
||||||
|
### Service Responsibilities
|
||||||
|
1. Wrap kubectl API access
|
||||||
|
2. Provide WebSocket multiplexer for:
|
||||||
|
- Shell sessions
|
||||||
|
- Log streaming
|
||||||
|
- Watch API updates
|
||||||
|
3. Handle CRDT sync for shared workspace state
|
||||||
|
4. Broadcast user presence/awareness
|
||||||
|
|
||||||
|
## Kubernetes Data Sources
|
||||||
|
|
||||||
|
### Required Resources
|
||||||
|
- Namespaces
|
||||||
|
- Deployments
|
||||||
|
- StatefulSets
|
||||||
|
- DaemonSets
|
||||||
|
- Services
|
||||||
|
- Ingresses
|
||||||
|
- ConfigMaps
|
||||||
|
- Secrets (with base64 decoding server-side)
|
||||||
|
- PVCs
|
||||||
|
- Events
|
||||||
|
- CRDs
|
||||||
|
|
||||||
|
### Live Updates
|
||||||
|
Implementation: Kubernetes Watch API
|
||||||
|
```
|
||||||
|
GET /api/v1/namespaces/{ns}/pods?watch=true
|
||||||
|
GET /api/v1/namespaces/{ns}/services?watch=true
|
||||||
|
GET /apis/apps/v1/namespaces/{ns}/deployments?watch=true
|
||||||
|
# etc for each resource type
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aggregation Strategy
|
||||||
|
- Single endpoint: `/api/watch` that multiplexes all watch streams
|
||||||
|
- Or: Resource-specific endpoints (`/api/watch/pods`, `/api/watch/services`, etc.)
|
||||||
|
- Include namespace filtering in requests
|
||||||
|
|
||||||
|
### Secret Decoding
|
||||||
|
- Option 1: Server-side decode before sending
|
||||||
|
- Option 2: Client-side decode (safer, never log)
|
||||||
|
- If client-side: Include decoded flag in response
|
||||||
|
- **Never log decoded values**
|
||||||
|
|
||||||
|
## WebSocket Endpoints
|
||||||
|
|
||||||
|
### Shell Endpoint
|
||||||
|
```
|
||||||
|
/ws/shell
|
||||||
|
```
|
||||||
|
- **Upstream**: `kubectl exec -it <pod> -n <ns> -- sh`
|
||||||
|
- **Protocol**: Bidirectional WebSocket
|
||||||
|
- Client → Server: Keycodes, terminal resize
|
||||||
|
- Server → Client: stdout, stderr
|
||||||
|
- **Terminal**: xterm.js on client
|
||||||
|
- **Backend**: Use k8s Go client's Exec API with streaming
|
||||||
|
|
||||||
|
### Logs Endpoint
|
||||||
|
```
|
||||||
|
/ws/logs
|
||||||
|
```
|
||||||
|
- **Upstream**: `kubectl logs --follow <pod> -n <ns>`
|
||||||
|
- **Protocol**: WebSocket text frames
|
||||||
|
- **Streaming**: Each line as separate message
|
||||||
|
- **Client**: Auto-scroll unless user scrolled up
|
||||||
|
|
||||||
|
### Watch Endpoint
|
||||||
|
```
|
||||||
|
/ws/watch
|
||||||
|
```
|
||||||
|
- **Purpose**: Broadcast resource changes to all connected clients
|
||||||
|
- **Payload**: JSON Patch or full object update
|
||||||
|
- **Filtering**: Per-client namespace/resource filters
|
||||||
|
|
||||||
|
## Multi-User Sync (The Yard)
|
||||||
|
|
||||||
|
### CRDT Strategy
|
||||||
|
- **Library**: Yjs (proven CRDT implementation)
|
||||||
|
- **Provider**:
|
||||||
|
- Single-node: in-memory or Redis
|
||||||
|
- Multi-node: y-leveldb, y-mongodb, or y-webrtc
|
||||||
|
- **WebSocket**: y-websocket for real-time sync
|
||||||
|
|
||||||
|
### Synced State
|
||||||
|
Only sync the *structure* of the workspace, not content:
|
||||||
|
- ✅ Krate positions (wx, wy)
|
||||||
|
- ✅ Krate existence (create/delete)
|
||||||
|
- ✅ Krate minimize state
|
||||||
|
- ✅ Window layout (grid positions, sizes)
|
||||||
|
- ✅ Room/canvas camera state (optional)
|
||||||
|
- ❌ Window content (logs, YAML, shell) — each client streams independently
|
||||||
|
|
||||||
|
### Presence/Awareness
|
||||||
|
Broadcast ephemeral user state:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
userId: string,
|
||||||
|
name: string,
|
||||||
|
color: string,
|
||||||
|
cursorPosition: { x, y } | null,
|
||||||
|
activeKrateId: string | null,
|
||||||
|
spotlightQuery: string | null,
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
- Each client publishes to Yjs Awareness channel
|
||||||
|
- Admin panel subscribes to awareness updates
|
||||||
|
- Update interval: every 5 seconds or on significant change
|
||||||
|
|
||||||
|
## Real-Time Shell Implementation
|
||||||
|
|
||||||
|
### Client-Side (xterm.js)
|
||||||
|
```
|
||||||
|
1. Connect WebSocket
|
||||||
|
2. Initialize xterm with {
|
||||||
|
rows: 24,
|
||||||
|
cols: 80,
|
||||||
|
fontFamily: 'IBM Plex Mono',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
3. Attach xterm to WebSocket
|
||||||
|
- term.write() → send to server
|
||||||
|
- WebSocket.onmessage → term.write()
|
||||||
|
4. Handle resize: term.resize(cols, rows)
|
||||||
|
5. Attach to DOM
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Side (Go + k8s client)
|
||||||
|
```
|
||||||
|
1. Extract pod/ns from URL: /ws/shell?pod=xxx&ns=yyy
|
||||||
|
2. Use k8s-exec Go library:
|
||||||
|
req := clientset.CoreV1().RESTClient().Post()
|
||||||
|
req.Name(pod).Namespace(ns).Resource("pods").SubResource("exec")
|
||||||
|
req.VersionedParams(&v1.ExecOptions{
|
||||||
|
Stdin: true,
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
Terminal: true,
|
||||||
|
}, scheme.ParameterCodec)
|
||||||
|
// Create exec stream
|
||||||
|
3. Duplex: WebSocket ↔ Exec stream
|
||||||
|
4. Handle close gracefully
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-Time Logs Implementation
|
||||||
|
|
||||||
|
### Client-Side
|
||||||
|
```
|
||||||
|
1. Connect WebSocket
|
||||||
|
2. Maintain scroll position state
|
||||||
|
3. On new message:
|
||||||
|
- Append to log buffer
|
||||||
|
- If wasAtBottom: scroll to bottom
|
||||||
|
- Else: keep existing scroll position
|
||||||
|
4. Colorize lines on receive (ERROR/WARN/INFO patterns)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Side
|
||||||
|
```
|
||||||
|
1. kubectl logs --follow logic via exec stream
|
||||||
|
2. Stream lines as WebSocket messages
|
||||||
|
3. Add keep-alive ping every 30s
|
||||||
|
4. Reconnect on disconnect (client-side)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend API Endpoints
|
||||||
|
|
||||||
|
### HTTP Endpoints (REST)
|
||||||
|
```
|
||||||
|
GET /api/cluster # Cluster metadata
|
||||||
|
GET /api/resources # All resources (cached, for initial load)
|
||||||
|
GET /api/resources/pods # Filtered by namespace (optional)
|
||||||
|
GET /api/resources/deployments
|
||||||
|
GET /api/resources/services
|
||||||
|
GET /api/resources/secrets
|
||||||
|
GET /api/resources/configmaps
|
||||||
|
GET /api/resources/namespaces
|
||||||
|
GET /api/resources/crds
|
||||||
|
GET /api/resource/{kind}/{name}?ns={namespace}
|
||||||
|
GET /api/health # Backend health check
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket Endpoints
|
||||||
|
```
|
||||||
|
/ws/shell # Shell session
|
||||||
|
/ws/logs # Log streaming
|
||||||
|
/ws/watch # Resource watch updates
|
||||||
|
/ws/sync # CRDT sync + awareness
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### WebSocket Disconnects
|
||||||
|
- Shell: Notify user, keep terminal buffer (don't clear)
|
||||||
|
- Logs: Auto-reconnect with exponential backoff
|
||||||
|
- Watch: Auto-reconnect, fetch delta on reconnect
|
||||||
|
|
||||||
|
### Backpressure
|
||||||
|
- Shell: Buffer output if client slow, drop old lines if buffer full
|
||||||
|
- Logs: Same strategy
|
||||||
|
- Watch: Batch updates if high frequency
|
||||||
|
|
||||||
|
### Connection Limits
|
||||||
|
- Per-user limits: max concurrent shells/logs per namespace
|
||||||
|
- Global limits: max connections
|
||||||
|
- Reject with 429 if limit exceeded
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- JWT or session cookie
|
||||||
|
- Validate token on WebSocket upgrade
|
||||||
|
- Bind connections to user ID
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
- Namespace-level RBAC
|
||||||
|
- User can only access resources they have permission for
|
||||||
|
- Backend must enforce (not just client-side filtering)
|
||||||
|
|
||||||
|
### Secret Handling
|
||||||
|
- Never log decoded secrets
|
||||||
|
- Sanitize before display
|
||||||
|
- Consider explicit user action to reveal (future enhancement)
|
||||||
|
|
||||||
|
## Scalability
|
||||||
|
|
||||||
|
### Single-Node (Development)
|
||||||
|
- In-memory Yjs provider
|
||||||
|
- Redis for presence
|
||||||
|
- Direct k8s API calls
|
||||||
|
|
||||||
|
### Multi-Node (Production)
|
||||||
|
- Shared Redis for Yjs CRDT state
|
||||||
|
- WebSocket leader election (one node handles all CRDT updates)
|
||||||
|
- or: y-webrtc for peer-to-peer (simpler, less reliable)
|
||||||
|
|
||||||
|
## Health Monitoring
|
||||||
|
- WebSocket connection count
|
||||||
|
- Active shell sessions
|
||||||
|
- API latency (p50, p95, p99)
|
||||||
|
- CRDT sync lag
|
||||||
|
- Error rates by endpoint
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
1. Unit tests for Kubernetes API wrappers
|
||||||
|
2. Integration tests with kind (Kubernetes in Docker)
|
||||||
|
3. Load tests for WebSocket connections
|
||||||
|
4. CRDT conflict resolution tests
|
||||||
105
design/canvas.md
Normal file
105
design/canvas.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Canvas Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The canvas is the infinite zoomable/pannable workspace where krates live. It serves as the primary interaction surface for exploring Kubernetes clusters.
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
- Render an infinite 2D workspace (12000×8000px world)
|
||||||
|
- Handle camera navigation (pan/zoom)
|
||||||
|
- Manage Level of Detail (LOD) based on zoom level
|
||||||
|
- Display a CAD grid overlay
|
||||||
|
- Coordinate global interactions (space panning, spotlight trigger)
|
||||||
|
|
||||||
|
## Technical Requirements
|
||||||
|
|
||||||
|
### Viewport & World Layer
|
||||||
|
- Full viewport (`position: fixed; inset: 0`)
|
||||||
|
- Background color: `#0b0e13`
|
||||||
|
- World layer: 12000×8000px div transformed via `translate(camX, camY) scale(zoom)`
|
||||||
|
- All krates/windows are absolutely positioned children
|
||||||
|
|
||||||
|
### Grid Overlay (CSS background-image)
|
||||||
|
- Fine grid: 34px spacing, rgba(125,145,175,.04)
|
||||||
|
- Coarse grid: 170px spacing, rgba(125,145,175,.075)
|
||||||
|
- Both X and Y repeating linear gradients
|
||||||
|
|
||||||
|
### Camera State
|
||||||
|
```typescript
|
||||||
|
interface CanvasState {
|
||||||
|
camX: number; // world-to-screen X offset
|
||||||
|
camY: number; // world-to-screen Y offset
|
||||||
|
zoom: number; // 0.16 – 2.2; default 0.92
|
||||||
|
flying: boolean; // true during animated camera transitions
|
||||||
|
dragging: boolean; // true during pan drag
|
||||||
|
collapsed: boolean; // LOD: true when zoom < 0.4
|
||||||
|
spacePanning: boolean; // true while space is held
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
### Scroll Wheel
|
||||||
|
- ** Default **: Pan canvas
|
||||||
|
- ** Ctrl/⌘ + scroll **: Zoom canvas (intercepted via wheel event with `e.ctrlKey || e.metaKey`)
|
||||||
|
|
||||||
|
### Space + Drag Panning
|
||||||
|
- Critical: must work even when shell window has keyboard focus
|
||||||
|
- Implementation:
|
||||||
|
- `document.addEventListener('keydown', handler, { capture: true })` on space
|
||||||
|
- Set "panning" flag, prevent default on body
|
||||||
|
- Forward `mousemove` to canvas pan handler
|
||||||
|
- Release on `keyup` for space
|
||||||
|
- Do NOT use `e.stopPropagation()` - allow event propagation
|
||||||
|
|
||||||
|
### Click/Typed Key Triggers
|
||||||
|
- ** Click on empty canvas **: Open spotlight
|
||||||
|
- ** Type any key **: Open spotlight with that character pre-seeded
|
||||||
|
|
||||||
|
### Zoom Levels / LOD
|
||||||
|
- ** zoom > 0.5 **: Normal view — krates expanded, all windows visible, draggable
|
||||||
|
- ** zoom < 0.4 **: Collapsed view — krates become 230px wide overview cards, no windows rendered
|
||||||
|
- ** Hysteresis**: Collapse at 0.4, re-expand at 0.5 (prevents flickering)
|
||||||
|
|
||||||
|
### Animations
|
||||||
|
- CSS `transform` with `transition` only for programmatic "fly" animations: `.52s cubic-bezier(.22,.8,.28,1)`
|
||||||
|
- During user scroll/drag: `transition: none` for immediacy
|
||||||
|
|
||||||
|
## UI Components
|
||||||
|
|
||||||
|
### Top Bar
|
||||||
|
- Position: `absolute; top: 0; left: 0; right: 0; z-index: 10; height ~56px`
|
||||||
|
- Layout: `display: flex; align-items: center; padding: 13px 18px`
|
||||||
|
- Background: none (transparent)
|
||||||
|
- Left side: Logo pill, cluster pill, krate count pill
|
||||||
|
- Right side: Synced pill, admin button, roster avatars
|
||||||
|
|
||||||
|
### Bottom Hint Bar
|
||||||
|
- Position: `absolute; right: 18px; bottom: 18px`
|
||||||
|
- Shows keyboard hints (status bar)
|
||||||
|
|
||||||
|
### Zoom Pill
|
||||||
|
- Position: `absolute; left: 18px; bottom: 18px`
|
||||||
|
- Displays current zoom percentage
|
||||||
|
|
||||||
|
### Minimap
|
||||||
|
- Position: `absolute; right: 18px; bottom: 64px`
|
||||||
|
- Size: 180×120px
|
||||||
|
- Shows all krates as small rectangles
|
||||||
|
- Viewport indicator rectangle showing current view frustum
|
||||||
|
- Click anywhere to fly camera to that world position
|
||||||
|
|
||||||
|
## Interaction Summary
|
||||||
|
| Interaction | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| Scroll wheel | Pan canvas |
|
||||||
|
| Ctrl/⌘ + scroll | Zoom canvas |
|
||||||
|
| Space + drag | Pan canvas (even over windows/shells) |
|
||||||
|
| Click empty canvas | Open spotlight |
|
||||||
|
| Type any key | Open spotlight pre-seeded |
|
||||||
|
| Minimap click | Fly camera to position |
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
1. **Space pan over shells**: Must intercept with `{ capture: true }`, prevent default, but don't stop propagation
|
||||||
|
2. **Ctrl/⌘+scroll**: Use `{ capture: true }` on canvas wheel handler to intercept before window scroll containers
|
||||||
|
3. **Camera transitions**: Only animate during programmatic fly, not user gestures
|
||||||
|
4. **LOD state**: Use stable boolean `collapsed` in state, not derived from `zoom` on every render
|
||||||
243
design/collection-window.md
Normal file
243
design/collection-window.md
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
# Collection Window Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
A collection window provides an overview of Kubernetes objects in a category (All Pods, All Services, etc.) or namespace. It supports both list and tree views with filtering and selection.
|
||||||
|
|
||||||
|
## Default Size
|
||||||
|
- Width: 540px
|
||||||
|
- Height: 480px (larger than detail windows for better data visibility)
|
||||||
|
|
||||||
|
## Header
|
||||||
|
|
||||||
|
### Title
|
||||||
|
- Format: `pods · ns/payments` or `All Pods`
|
||||||
|
- Bold styling
|
||||||
|
|
||||||
|
### Status Summary
|
||||||
|
- Health badges: `✓ N / ⚠ N / ✗ N`
|
||||||
|
- Colors: OK=green, Warn=amber, Error=red
|
||||||
|
- Order: Ready/Warn/Error counts
|
||||||
|
|
||||||
|
### Filter Input
|
||||||
|
- Auto-focused on open
|
||||||
|
- Styling:
|
||||||
|
- `font-family: IBM Plex Mono`
|
||||||
|
- `font-size: 12px`
|
||||||
|
- Placeholder: `Filter...`
|
||||||
|
- Instant filtering (no debounce needed for <1000 items)
|
||||||
|
|
||||||
|
### View Toggle
|
||||||
|
- **List mode** button: `list icon`
|
||||||
|
- **Tree mode** button: `tree icon`
|
||||||
|
- Toggles between list and tree display
|
||||||
|
- Keyboard shortcut: `t` or click toggle button
|
||||||
|
|
||||||
|
### Keyboard Hint
|
||||||
|
- Always visible, muted text: `↑↓ ⌥L/S/D/Y`
|
||||||
|
- Shows navigation + view shortcuts
|
||||||
|
|
||||||
|
## List Mode
|
||||||
|
|
||||||
|
### Row Structure
|
||||||
|
Each row displays:
|
||||||
|
- Shape glyph (8px, color-coded by object type)
|
||||||
|
- Name (primary text)
|
||||||
|
- Relation tag (muted, for tree mode)
|
||||||
|
- Health metric (icon or text)
|
||||||
|
- Type badge (small, muted)
|
||||||
|
- View buttons: `⌥L`, `⌥S`, `⌥D`, `⌥Y` (hover show tooltips)
|
||||||
|
- Status dot (green/amber/red)
|
||||||
|
|
||||||
|
### Selection
|
||||||
|
- Selected row:
|
||||||
|
- Background: `accentDim` (subtle accent color)
|
||||||
|
- Left border: `2px solid accent`
|
||||||
|
- Hover row:
|
||||||
|
- Background: `rgba(255,255,255,.04)`
|
||||||
|
- Border-radius: 4px
|
||||||
|
|
||||||
|
### Sorting
|
||||||
|
1. Primary: Health status
|
||||||
|
- Degraded/failed first
|
||||||
|
- Then pending
|
||||||
|
- Then ready/running
|
||||||
|
2. Secondary: Alphabetical within each group
|
||||||
|
|
||||||
|
## Tree Mode (k9s xray-style)
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
- **Workloads expand to pods**:
|
||||||
|
- Deployment → pods
|
||||||
|
- StatefulSet → pods
|
||||||
|
- DaemonSet → pods
|
||||||
|
- **Pods expand to resources**:
|
||||||
|
- Pods show configMaps, secrets, PVCs
|
||||||
|
- **Services/Ingresses expand to targets**:
|
||||||
|
- Show selectors
|
||||||
|
- Show endpoints
|
||||||
|
|
||||||
|
### Relation Tags
|
||||||
|
Show as small icons or labels:
|
||||||
|
- `pod` (attached to pod rows)
|
||||||
|
- `configMap` (mounted volumes)
|
||||||
|
- `secret` (mounted secrets)
|
||||||
|
- `volume` (PVC)
|
||||||
|
- `selects` (service → pods)
|
||||||
|
- `routes` (ingress → service)
|
||||||
|
- `used by` (reverse relationships)
|
||||||
|
|
||||||
|
### Indentation
|
||||||
|
- Per depth: `20px` indentation
|
||||||
|
- Connector: L-shaped border element (vertical + horizontal)
|
||||||
|
- Visual hierarchy clear at a glance
|
||||||
|
|
||||||
|
### Expand/Collapse
|
||||||
|
- Click chevron/arrow: Toggle children
|
||||||
|
- Double-click name: Toggle
|
||||||
|
- Click anywhere else on row: Select row
|
||||||
|
|
||||||
|
### Filtering in Tree Mode
|
||||||
|
- Matching nodes + **all their ancestors** must stay visible
|
||||||
|
- Ancestors shown even if not matching, but with different styling
|
||||||
|
- Children hidden if no match
|
||||||
|
- Preserves hierarchy readability
|
||||||
|
|
||||||
|
## Keyboard Navigation
|
||||||
|
|
||||||
|
### List/Tree Mode
|
||||||
|
| Key | Context | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| ↑ / ↓ | Collection focused | Move row selection |
|
||||||
|
| PageUp / PageDown | Collection focused | Move selection by page |
|
||||||
|
| Home / End | Collection focused | First / last row |
|
||||||
|
| Enter | Row selected | Open default view for selected row |
|
||||||
|
| ⌥L / ⌥S / ⌥D / ⌥Y | Row selected | Open view for selected row |
|
||||||
|
| Esc | Filter focused | Blur filter input |
|
||||||
|
| Esc | Collection focused | No-op (handle in krate) |
|
||||||
|
| t | Anywhere | Toggle list/tree view |
|
||||||
|
|
||||||
|
### Mouse + Type Routing
|
||||||
|
- Mouse-over collection window + type = focus filter input
|
||||||
|
- Similar to shell routing but for filter input
|
||||||
|
- Use `hoveredWindowId` ref to determine routing
|
||||||
|
|
||||||
|
## View Shortcuts
|
||||||
|
|
||||||
|
### Key Mapping (All use ⌥ Alt)
|
||||||
|
- `⌥L`: Open logs view (if supported)
|
||||||
|
- `⌥S`: Open shell view (if supported)
|
||||||
|
- `⌥D`: Open describe view
|
||||||
|
- `⌥Y`: Open YAML view
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Event listener with `event.code` for layout safety
|
||||||
|
- Open views in newly created krate (or existing if spotlight session)
|
||||||
|
- Shell tab disabled for non-exec types (opacity 0.45)
|
||||||
|
|
||||||
|
## View-Specific Content
|
||||||
|
|
||||||
|
### Logs View (Pods/Workloads Only)
|
||||||
|
- **Streaming**: WebSocket → `kubectl logs --follow`
|
||||||
|
- **Auto-scroll**: To bottom unless user scrolled up
|
||||||
|
- **Color coding**:
|
||||||
|
- ERROR: `#ef6f6f`
|
||||||
|
- WARN: `#e8b54a`
|
||||||
|
- INFO: `#b9c6d8`
|
||||||
|
- **Timestamps**: Optional toggle (hidden by default)
|
||||||
|
|
||||||
|
### Shell View (Pods Only)
|
||||||
|
- **Implementation**: xterm.js
|
||||||
|
- **Disabled**: For non-pod workloads, show disabled (muted)
|
||||||
|
- **Cursor**: Blinking block cursor
|
||||||
|
- **Key routing**: Route to shell when collected window hovered
|
||||||
|
|
||||||
|
### Describe View
|
||||||
|
- Human-readable object description
|
||||||
|
- All metadata sections
|
||||||
|
- Formatted for easy scanning
|
||||||
|
|
||||||
|
### YAML View
|
||||||
|
- Full object YAML
|
||||||
|
- **Secrets**: Decode data values
|
||||||
|
- Banner: `⊙ secret values auto-decoded`
|
||||||
|
- Monospace font, line numbers optional
|
||||||
|
|
||||||
|
## Filter Behavior
|
||||||
|
|
||||||
|
### Instant Filtering
|
||||||
|
- No debounce (fast for <1000 items)
|
||||||
|
- Debounce if dataset grows: 16ms
|
||||||
|
|
||||||
|
### Filter Logic
|
||||||
|
- Case-insensitive substring match
|
||||||
|
- Match against: name, labels, annotations, namespace
|
||||||
|
- For tree mode: Match name only (not ancestors)
|
||||||
|
|
||||||
|
### Clear Filter
|
||||||
|
- Esc key (when filter focused)
|
||||||
|
- Clear button (×) in input
|
||||||
|
- Empty string clears filter
|
||||||
|
|
||||||
|
### Keyboard Focus
|
||||||
|
1. Click collection window + type = focus filter
|
||||||
|
2. Click filter input = focus
|
||||||
|
3. Click anywhere else = blur filter, focus canvas shortcuts
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Per-Window State
|
||||||
|
```typescript
|
||||||
|
interface CollectionState {
|
||||||
|
wid: string; // window ID
|
||||||
|
search: string; // filter text
|
||||||
|
view: 'list' | 'tree';
|
||||||
|
sel: number; // selected row index
|
||||||
|
expanded: Set<string>; // tree expansion state (node IDs)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Loading
|
||||||
|
```typescript
|
||||||
|
interface CollectionData {
|
||||||
|
kind: string; // 'Pod', 'Service', etc.
|
||||||
|
namespace?: string; // undefined for "All Pods"
|
||||||
|
items: K8sObject[];
|
||||||
|
filtered: K8sObject[]; // after filter
|
||||||
|
treeData?: TreeNode[]; // tree mode intermediate
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tree Node Structure
|
||||||
|
```typescript
|
||||||
|
interface TreeNode {
|
||||||
|
id: string; // unique within collection
|
||||||
|
name: string;
|
||||||
|
kind: string; // 'Pod', 'Deployment', etc.
|
||||||
|
obj: K8sObject | null; // null for synthetic nodes (e.g., "pods")
|
||||||
|
depth: number;
|
||||||
|
parentId: string | null;
|
||||||
|
children: string[]; // node IDs
|
||||||
|
relation: string; // 'pod', 'configMap', 'selects', etc.
|
||||||
|
hasChildren: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Large Datasets
|
||||||
|
- List virtualization: Only render visible rows (window height / row height)
|
||||||
|
- Tree optimization: Lazy expand (fetch children on expand, not preload)
|
||||||
|
- Filtering: Use efficient algorithms (no regex, simple string.match)
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
- Throttle tree rebuild: 100ms
|
||||||
|
- Only rebuild if filter or data changed
|
||||||
|
- Keep expansion state on update (if node still exists)
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
1. **Tree expansion**: Keep ancestors visible when filtering
|
||||||
|
2. **Auto-focus**: Focus filter input on window open (use ref + useEffect)
|
||||||
|
3. **View shortcuts**: Use `event.code` (layout-safe)
|
||||||
|
4. **Sorting priority**: Health first, then alphabetical
|
||||||
|
5. **Shell vs other types**: Disable shell tab for non-pod workloads
|
||||||
|
6. **Filter scope**: In tree mode, filter only on names, not on ancestors
|
||||||
176
design/detail-window.md
Normal file
176
design/detail-window.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Detail Window Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
A detail window shows one view (YAML / Describe / Logs / Shell) for one Kubernetes object. Windows are grouped into krates and can be resized/moved within their container.
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
- Display object data in one of four view types
|
||||||
|
- Allow window dragging within krate
|
||||||
|
- Support window resizing with proportional sibling adjustment
|
||||||
|
- Implement maximize/restore functionality
|
||||||
|
- Route keyboard focus on mouse-over
|
||||||
|
|
||||||
|
## Window Types
|
||||||
|
|
||||||
|
### Detail View (default)
|
||||||
|
- **Purpose**: Display static object information
|
||||||
|
- **Content types**: YAML, Describe, Logs (for pods/workloads)
|
||||||
|
|
||||||
|
### Collection Window
|
||||||
|
- **Purpose**: List/Tree view of multiple objects
|
||||||
|
- **Special features**: Filter input, selection highlighting, view shortcuts
|
||||||
|
|
||||||
|
## Window anatomy (380×362px default)
|
||||||
|
|
||||||
|
### Header (36px height)
|
||||||
|
- Left: Shape glyph (8px, color-coded by object type)
|
||||||
|
- Middle: View label (uppercase, accent-colored) + object name + status badge
|
||||||
|
- Right: × button (close window)
|
||||||
|
- **Cursor**: `grab` — drag to reposition within canvas
|
||||||
|
- **Double-click header**: Maximize / restore window
|
||||||
|
- **Drag**: Move window within krate
|
||||||
|
|
||||||
|
### Tab Bar
|
||||||
|
- Tabs: YAML · Describe · Logs · Shell
|
||||||
|
- Shell tab: disabled (opacity 0.45) for non-exec object types
|
||||||
|
- Active tab: accent underline/highlight
|
||||||
|
- Click tab: Switch view content
|
||||||
|
|
||||||
|
### Content Area (286px height)
|
||||||
|
- Element: `<pre>` (preformatted text)
|
||||||
|
- Styling:
|
||||||
|
- `overflow: auto`
|
||||||
|
- `height: 286px`
|
||||||
|
- `font-family: 'IBM Plex Mono'`
|
||||||
|
- `font-size: 12px`
|
||||||
|
- `line-height: 1.65`
|
||||||
|
- Scrollbar:
|
||||||
|
- Width: 8px
|
||||||
|
- Color: rgba(140,165,200,.18)
|
||||||
|
|
||||||
|
## Resize Behavior
|
||||||
|
|
||||||
|
### Resize Handle
|
||||||
|
- Positioned at bottom-right corner
|
||||||
|
- Drag to resize
|
||||||
|
- **Proportional adjustment**: When one window resized, siblings in same column/row resize proportionally
|
||||||
|
- Storage: Use `rowHeights` and `colWidths` arrays per krate (future enhancement)
|
||||||
|
|
||||||
|
### Window Grid
|
||||||
|
- **Column pitch**: 412px
|
||||||
|
- **Row pitch**: 416px
|
||||||
|
- **Window default**: 380×362px (leaves room for grip/padding)
|
||||||
|
|
||||||
|
## Maximize Functionality
|
||||||
|
|
||||||
|
### Maximize State
|
||||||
|
- Width: `viewport.width - 44px` (22px margins)
|
||||||
|
- Height: `viewport.height - topBarHeight(68px) - bottomBarHeight(66px)`
|
||||||
|
- Camera adjusted: `camX = 22 - krate.wx - window.dx`, `camY = 68 - krate.wy - window.dy`, `zoom = 1`
|
||||||
|
|
||||||
|
### Trigger
|
||||||
|
- **Key**: `z` (when mouse is over window)
|
||||||
|
- **Mouse**: Double-click header
|
||||||
|
- Restore: Same trigger again
|
||||||
|
|
||||||
|
## Keyboard Focus Routing
|
||||||
|
|
||||||
|
### Mouse-Over Detection
|
||||||
|
- Track `mouseenter`/`mouseleave` per window
|
||||||
|
- Set `hoveredWindowId` ref
|
||||||
|
- Clear on `mouseleave`
|
||||||
|
|
||||||
|
### Key Event Routing
|
||||||
|
Global `keydown` handler with `{ capture: true }`:
|
||||||
|
```
|
||||||
|
if (hoveredWindowId is shell) {
|
||||||
|
route keypress to shell
|
||||||
|
} else if (hoveredWindowId is collection) {
|
||||||
|
route keypress to filter input
|
||||||
|
} else {
|
||||||
|
let it fall through to canvas shortcuts
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell Special Case
|
||||||
|
- Space character goes to terminal (NOT spotlight)
|
||||||
|
- Must distinguish between canvas space-pan and shell space-input
|
||||||
|
|
||||||
|
## View-Specific Content
|
||||||
|
|
||||||
|
### YAML View
|
||||||
|
- Display full object YAML
|
||||||
|
- **Secret objects**: Base64-decode all data values
|
||||||
|
- Show banner: `⊙ secret values auto-decoded`
|
||||||
|
- Security: Handle carefully, never log decoded values
|
||||||
|
|
||||||
|
### Describe View
|
||||||
|
- Human-readable object description
|
||||||
|
- Formatted for readability
|
||||||
|
- All metadata sections included
|
||||||
|
|
||||||
|
### Logs View
|
||||||
|
- **Pods/workloads only**
|
||||||
|
- **Live-tailing**: Stream via WebSocket
|
||||||
|
- Auto-scroll to bottom on new lines (unless user scrolled up)
|
||||||
|
- Color-code:
|
||||||
|
- ERROR: `#ef6f6f`
|
||||||
|
- WARN: `#e8b54a`
|
||||||
|
- INFO: `#b9c6d8`
|
||||||
|
|
||||||
|
### Shell View
|
||||||
|
- **Implementation**: xterm.js (real terminal)
|
||||||
|
- **Backend**: WebSocket → `kubectl exec -it <pod> -n <ns> -- sh`
|
||||||
|
- **Keyboard routing**: Route keypresses to terminal, passthrough terminal output
|
||||||
|
- **Scroll wheel**: Scroll terminal content (NOT canvas pan)
|
||||||
|
- **Ctrl/⌘+scroll**: Zoom canvas (not terminal zoom)
|
||||||
|
|
||||||
|
## Collection Window Specifics
|
||||||
|
|
||||||
|
### Size
|
||||||
|
- Default: 540×480px (larger than detail windows)
|
||||||
|
|
||||||
|
### Header
|
||||||
|
- Title: `pods · ns/payments`
|
||||||
|
- Status summary badges: `✓ N / ⚠ N / ✗ N`
|
||||||
|
- Filter input (auto-focused on open)
|
||||||
|
- List / Tree toggle buttons
|
||||||
|
- Keyboard hint: `↑↓ ⌥L/S/D/Y`
|
||||||
|
|
||||||
|
### List Mode
|
||||||
|
- Rows: shape glyph + name + health metric + type badge + view buttons + status dot
|
||||||
|
- Selected row: `background: accentDim; border-left: 2px solid accent`
|
||||||
|
- Hover row: `background: rgba(255,255,255,.04)`
|
||||||
|
- Sorted: degraded/failed first, then pending, then ready; alpha within groups
|
||||||
|
|
||||||
|
### Tree Mode (k9s xray)
|
||||||
|
- Workloads expand to show pods
|
||||||
|
- Pods expand to show configMaps, secrets, PVCs
|
||||||
|
- Services/ingresses expand to show targets
|
||||||
|
- Relation tags: `pod`, `configMap`, `secret`, `volume`, `selects`, `routes`, `used by`
|
||||||
|
- Indent: `20px * depth`
|
||||||
|
- Filtering: Keep matching nodes + all ancestors for hierarchy
|
||||||
|
|
||||||
|
## Window State
|
||||||
|
```typescript
|
||||||
|
interface Window {
|
||||||
|
wid: string; // unique window ID
|
||||||
|
kind: 'detail' | 'collection';
|
||||||
|
tab: 'logs' | 'shell' | 'describe' | 'yaml';
|
||||||
|
dx: number; // offset within krate (column * 412)
|
||||||
|
dy: number; // offset within krate (row * 416)
|
||||||
|
w: number; // width
|
||||||
|
h: number; // height
|
||||||
|
isMaximized: boolean; // current state
|
||||||
|
prevW?: number; // previous width (for restore)
|
||||||
|
prevH?: number; // previous height (for restore)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
1. **Space key**: Must distinguish between canvas space-pan (capture with `{ capture: true }`) and shell space-input
|
||||||
|
2. **Ctrl/⌘+scroll**: Use `{ capture: true }` on canvas to intercept before window scroll containers
|
||||||
|
3. **Secret decoding**: Do server-side or careful client-side; never log decoded values
|
||||||
|
4. **Window resizing**: Proportional siblings requires storing `rowHeights`/`colWidths` arrays (not just individual `dx/dy`)
|
||||||
|
5. **Focus routing**: Mouse-over + type should focus shell/filter, not trigger canvas shortcuts
|
||||||
|
6. **Collection auto-focus**: On creation, focus filter input in next tick using ref + `useEffect`
|
||||||
101
design/krate.md
Normal file
101
design/krate.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Krate (Window Group) Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
A krate is a named group of detail windows for a single Kubernetes object or collection. Krates live on the canvas and serve as organizational units for related workspace views.
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
- Group related windows together visually and logically
|
||||||
|
- Auto-fit frame around contained windows
|
||||||
|
- Manage window layout within krate boundaries
|
||||||
|
- Handle krate-level interactions (move, drag, collapse, expand)
|
||||||
|
|
||||||
|
## Visual Container
|
||||||
|
|
||||||
|
### Krate Frame
|
||||||
|
- **Dashed border**: `border: 1px dashed rgba(color, .3)`, `border-radius: 18px`
|
||||||
|
- **Background**: `rgba(color, .04)` (subtle color tint)
|
||||||
|
- **Auto-fit**: Frame adjusts to bounding box of all windows + 30px padding
|
||||||
|
- **Color**: Matches namespace palette (e.g., payments: `#6fb1ff`)
|
||||||
|
|
||||||
|
### Header Bar
|
||||||
|
Above the frame:
|
||||||
|
- Shows object name + status badge + window count
|
||||||
|
- **Drag handle**: Move entire krate
|
||||||
|
- **× button**: Dismiss krate
|
||||||
|
- **Minimize button** (`—`): Collapse to overview card
|
||||||
|
- **Double-click header**: Collapse/expand toggle
|
||||||
|
|
||||||
|
## Window Layout
|
||||||
|
|
||||||
|
### Grid System
|
||||||
|
- Windows tiled in 2-column grid
|
||||||
|
- Column 0: `dx: 0`
|
||||||
|
- Column 1: `dx: 412`
|
||||||
|
- Row height: 416px
|
||||||
|
- **Window default size**: 380×362px
|
||||||
|
|
||||||
|
### Properties
|
||||||
|
- Windows never overlap (snap-to-grid on release)
|
||||||
|
- When one window resized, siblings resize proportionally in same column/row
|
||||||
|
- Storage:
|
||||||
|
```typescript
|
||||||
|
interface Window {
|
||||||
|
wid: string;
|
||||||
|
kind: 'detail' | 'collection';
|
||||||
|
tab: 'logs' | 'shell' | 'describe' | 'yaml';
|
||||||
|
dx: number; // offset within krate (column * 412)
|
||||||
|
dy: number; // offset within krate (row * 416)
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Krate Interactions
|
||||||
|
|
||||||
|
### Dragging
|
||||||
|
- **Drag header** = move entire krate
|
||||||
|
- **Non-overlapping placement**: On drag release, nudge to avoid overlap with other krates
|
||||||
|
- **Gentle snap, not strict grid** (allows flexibility while preventing overlap)
|
||||||
|
|
||||||
|
### Collapse/Expand
|
||||||
|
- **Minimized state**: 230px-wide card showing:
|
||||||
|
- Name
|
||||||
|
- View-letter badges (Y/D/L/S)
|
||||||
|
- Status dot
|
||||||
|
- Window count
|
||||||
|
- Namespace
|
||||||
|
- **Double-click collapsed card**: Re-expand and fly camera to krate
|
||||||
|
|
||||||
|
### Camera Fly
|
||||||
|
- On krate creation: animate camera to center krate at `zoom: 0.92`
|
||||||
|
- Duration: `.52s cubic-bezier(.22,.8,.28,1)`
|
||||||
|
- Position: center of krate's bounding box
|
||||||
|
|
||||||
|
## Krate State
|
||||||
|
```typescript
|
||||||
|
interface Krate {
|
||||||
|
id: string;
|
||||||
|
objId: string | null; // null for collection krates
|
||||||
|
collScope?: { kind: 'namespace' | 'category', value: string };
|
||||||
|
label: string; // display name
|
||||||
|
status: string; // status badge text
|
||||||
|
color: string; // hex, from namespace palette
|
||||||
|
wx: number; wy: number; // world position (top-left of frame)
|
||||||
|
minimized: boolean;
|
||||||
|
windows: Window[];
|
||||||
|
seq: number; // for generating unique window IDs
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-Overlapping Placement Algorithm
|
||||||
|
1. Try computed default position
|
||||||
|
2. Check bounding boxes against all existing krates
|
||||||
|
3. If overlap, nudge by fixed increments (e.g., 20px right/down)
|
||||||
|
4. Repeat until no overlap
|
||||||
|
5. **Same algorithm** used on creation AND drag release
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
1. **Auto-fit frame**: Re-compute bounding box whenever windows are added/removed/moved/resized
|
||||||
|
2. **Non-overlapping**: Use AABB (Axis-Aligned Bounding Box) collision detection
|
||||||
|
3. **Window grid**: Store `colWidths` and `rowHeights` arrays for proper resize relationships (future enhancement)
|
||||||
|
4. **Krate ID**: `objId` is null for collections; otherwise references the Kubernetes object ID
|
||||||
149
design/minimap.md
Normal file
149
design/minimap.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Minimap Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The minimap provides a bird's-eye overview of all krates' positions on the canvas. It allows navigation by clicking to fly the camera to any location.
|
||||||
|
|
||||||
|
## Position & Size
|
||||||
|
|
||||||
|
### Placement
|
||||||
|
- Position: `absolute; right: 18px; bottom: 64px` (above zoom pill)
|
||||||
|
- Size: 180×120px
|
||||||
|
- Z-index: Must be above canvas but below spotlight (z-index: 20-30 range)
|
||||||
|
|
||||||
|
### Background
|
||||||
|
- Base: `rgba(16,20,28,.97)`
|
||||||
|
- Border: `1px solid rgba(140,165,200,.2)`
|
||||||
|
- Border-radius: `4px`
|
||||||
|
- Padding: None (full content area)
|
||||||
|
|
||||||
|
## Viewport Indicator
|
||||||
|
|
||||||
|
### Rectangle
|
||||||
|
- Shows current camera view frustum
|
||||||
|
- Position: Relative to minimap content (proportionally mapped)
|
||||||
|
- Size: Proportional to zoom level
|
||||||
|
- Color: Accent (e.g., `#4dd6e8` cyan) with 40% opacity
|
||||||
|
- Stroke: Solid line, 1px
|
||||||
|
|
||||||
|
### Mapping
|
||||||
|
- World position (wx, wy) → Minimap coordinates
|
||||||
|
- Formula:
|
||||||
|
```
|
||||||
|
mx = (wx + camX) / zoom scaling factor
|
||||||
|
my = (wy + camY) / zoom scaling factor
|
||||||
|
```
|
||||||
|
- Zoom mapping: Larger viewport indicator = lower zoom level
|
||||||
|
|
||||||
|
## Krate Visualization
|
||||||
|
|
||||||
|
### Representation
|
||||||
|
- Each krate = small rectangle (8×6px default)
|
||||||
|
- Color: Matches krate's namespace color
|
||||||
|
- Position: Relative to minimap origin
|
||||||
|
- Scaled down: ~1/60th of world space
|
||||||
|
|
||||||
|
### Content
|
||||||
|
- Krate positions only (not internal windows)
|
||||||
|
- All active krates on canvas
|
||||||
|
- Update on: krate create, delete, move
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
- Only render visible krates (cull distant ones)
|
||||||
|
- Batch updates (throttle minimap render)
|
||||||
|
- Use CSS transform, not re-layout
|
||||||
|
|
||||||
|
## Interaction
|
||||||
|
|
||||||
|
### Click Navigation
|
||||||
|
- Click anywhere → fly camera to world position
|
||||||
|
- Calculate world coordinates from click position:
|
||||||
|
```
|
||||||
|
worldX = (clickX / minimapWidth) * worldWidth - viewportWidth/2
|
||||||
|
worldY = (clickY / minimapHeight) * worldHeight - viewportHeight/2
|
||||||
|
```
|
||||||
|
- Animate camera fly: `.52s cubic-bezier(.22,.8,.28,1)`
|
||||||
|
- Zoom: Set to `0.92` or user's preferred zoom
|
||||||
|
|
||||||
|
### Hover Effects (Optional)
|
||||||
|
- Hover krate rectangle → highlight preview on canvas
|
||||||
|
- Show tooltip: krate name, window count
|
||||||
|
|
||||||
|
## Canvas Synchronization
|
||||||
|
|
||||||
|
### Updates Triggered By
|
||||||
|
1. New krate creation
|
||||||
|
2. Krate movement (drag)
|
||||||
|
3. Krate deletion
|
||||||
|
4. Canvas resize (re-calculate scale)
|
||||||
|
5. Camera change (update viewport indicator)
|
||||||
|
|
||||||
|
### Throttling
|
||||||
|
- Debounce minimap render: 100ms
|
||||||
|
- Don't update on every pixel of drag
|
||||||
|
- Batch: Collect all krate changes, render once
|
||||||
|
|
||||||
|
## Visual Styling
|
||||||
|
|
||||||
|
### Krate Rectangles
|
||||||
|
- Size: 8×6px (small but visible)
|
||||||
|
- Color: Krate's namespace color
|
||||||
|
- Fill: Solid
|
||||||
|
- Border: None (or very subtle: rgba(255,255,255,.1))
|
||||||
|
|
||||||
|
### Viewport Indicator
|
||||||
|
- Fill: None
|
||||||
|
- Stroke: `#4dd6e8` (accent), 1px
|
||||||
|
- Opacity: 0.4
|
||||||
|
- Rounded corners: 2px
|
||||||
|
|
||||||
|
### Background Patterns (Optional)
|
||||||
|
- Subtle grid overlay (same as main canvas, fainter)
|
||||||
|
- Stars or dots for visual interest
|
||||||
|
- Very low opacity: rgba(255,255,255,.05)
|
||||||
|
|
||||||
|
## Coordinate Mapping
|
||||||
|
|
||||||
|
### World to Minimap
|
||||||
|
```
|
||||||
|
minimapScale = minimapWidth / worldWidth (e.g., 180 / 12000 = 0.015)
|
||||||
|
minimapX = (worldX + camX) * minimapScale
|
||||||
|
minimapY = (worldY + camY) * minimapScale
|
||||||
|
```
|
||||||
|
|
||||||
|
### Minimap to World (for click)
|
||||||
|
```
|
||||||
|
worldX = (minimapX / minimapScale) - camX
|
||||||
|
worldY = (minimapY / minimapScale) - camY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewport Rect
|
||||||
|
```
|
||||||
|
viewportWidthWorld = viewportWidth / zoom
|
||||||
|
viewportHeightWorld = viewportHeight / zoom
|
||||||
|
|
||||||
|
viewportMinimapX = (camX * minimapScale)
|
||||||
|
viewportMinimapY = (camY * minimapScale)
|
||||||
|
viewportMinimapW = (viewportWidthWorld * minimapScale)
|
||||||
|
viewportMinimapH = (viewportHeightWorld * minimapScale)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Animation
|
||||||
|
|
||||||
|
### Camera Fly
|
||||||
|
- Duration: `.52s`
|
||||||
|
- Easing: `cubic-bezier(.22,.8,.28,1)`
|
||||||
|
- Target: Center minimap-clicked position
|
||||||
|
- Zoom: Set to `0.92` (or user preference)
|
||||||
|
|
||||||
|
### Smooth Updates
|
||||||
|
- Smooth viewport indicator movement: CSS transition
|
||||||
|
- Avoid jitter during drag
|
||||||
|
- Use `requestAnimationFrame` for updates
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
1. **Coordinate precision**: Use floating-point for accurate mapping
|
||||||
|
2. **Zoom handling**: Viewport indicator must scale inversely with zoom
|
||||||
|
3. **Performance**: Minimap should never block main thread
|
||||||
|
4. **Culling**: Don't render krates far outside view
|
||||||
|
5. **Click handling**: Ensure click z-index > canvas but < spotlight
|
||||||
|
6. **Responsiveness**: Recalculate mapping on window resize
|
||||||
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)
|
||||||
137
design/spotlight.md
Normal file
137
design/spotlight.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Spotlight Search Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The spotlight is the global search overlay that allows users to find any Kubernetes object or existing krate. It provides fuzzy-ranked search results with quick view shortcuts.
|
||||||
|
|
||||||
|
## Core Responsibilities
|
||||||
|
- Display search results ranked by fuzzy matching
|
||||||
|
- Support typed filters (Tab to cycle through types)
|
||||||
|
- Provide view shortcuts (Logs, Shell, Describe, YAML)
|
||||||
|
- Auto-close after keyboard idle and open selected object
|
||||||
|
|
||||||
|
## UI Components
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
- Full-screen backdrop: `rgba(7,9,13,.55)`, `backdrop-filter: blur(2px)`
|
||||||
|
- Search panel: `width: min(660px, 93vw)`, `top: 15%`, centered horizontally
|
||||||
|
- Panel: `background: rgba(16,20,28,.97)`, `border: 1px solid rgba(140,165,200,.26)`, `border-radius: 14px`
|
||||||
|
|
||||||
|
### Sections (top→bottom)
|
||||||
|
1. **Input row** (~52px):
|
||||||
|
- `⌕` glyph (accent color)
|
||||||
|
- Optional type-filter pill
|
||||||
|
- Input field (`font-size: 18px`, IBM Plex Sans)
|
||||||
|
- Optional Tab ghost hint
|
||||||
|
|
||||||
|
2. **Type chips row** (scrollable):
|
||||||
|
- All / deploy / svc / pods / secrets / config / sts / crd / namespace / ns
|
||||||
|
- Scrollable horizontally on small screens
|
||||||
|
- Shows selected filter as pill
|
||||||
|
|
||||||
|
3. **Results list** (`max-height: 48vh, overflow: auto`):
|
||||||
|
- Each row: shape glyph + name + subtext (type · ns/namespace) + CRD badge + type badge
|
||||||
|
- Selected row highlight: accent background
|
||||||
|
- Scrollbar styled (8px, rgba(140,165,200,.18))
|
||||||
|
|
||||||
|
4. **View chips** (expanded on selected row only):
|
||||||
|
- `⌥L logs · ⌥S shell · ⌥D describe · ⌥Y yaml`
|
||||||
|
- Only available views shown (YAML and Describe always shown; Logs and Shell for pods/deployments/daemonsets/statefulsets)
|
||||||
|
- Click to open that view for selected result
|
||||||
|
|
||||||
|
5. **Footer**:
|
||||||
|
- `↑↓ pick · ⌥L/S/D/Y open views · ⏎ open default · ⇥ filter type · esc done`
|
||||||
|
|
||||||
|
## Fuzzy Search Algorithm
|
||||||
|
|
||||||
|
### Scoring Formula
|
||||||
|
```
|
||||||
|
Score = character-match score × 10 + type boost
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Boosts
|
||||||
|
- CRD: 60
|
||||||
|
- Deployment: 50
|
||||||
|
- StatefulSet: 48
|
||||||
|
- Service: 46
|
||||||
|
- DaemonSet: 44
|
||||||
|
- Ingress: 42
|
||||||
|
- Secret: 24
|
||||||
|
- ConfigMap: 22
|
||||||
|
- PVC: 18
|
||||||
|
- Pod: 16
|
||||||
|
|
||||||
|
### Character Matching
|
||||||
|
- Sequential character match
|
||||||
|
- Bonus for consecutive matches: +3
|
||||||
|
- Bonus for word-boundary starts: +5
|
||||||
|
|
||||||
|
### Results
|
||||||
|
- Capped at 8 results
|
||||||
|
- Debounce: 16ms (one animation frame) for performance with large datasets
|
||||||
|
|
||||||
|
## Type Filter
|
||||||
|
|
||||||
|
### Tab Quick-Filter
|
||||||
|
- Typing type alias (e.g., `pod`, `svc`) shows ghost hint: `⇥ Pods`
|
||||||
|
- Tab locks filter as pill
|
||||||
|
- Backspace removes filter (only when empty query and filter active)
|
||||||
|
- Tab cycles through type filters (most-recently-opened first)
|
||||||
|
|
||||||
|
## View Shortcuts
|
||||||
|
|
||||||
|
### Key Mapping
|
||||||
|
All use **⌥ (Alt/Option)**, NOT Ctrl (reserved for terminal):
|
||||||
|
- `⌥L` = logs
|
||||||
|
- `⌥S` = shell
|
||||||
|
- `⌥D` = describe
|
||||||
|
- `⌥Y` = yaml
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- Use `event.code` (layout-safe, e.g., `KeyL`), not `event.key`
|
||||||
|
- Multiple views can be opened before closing spotlight
|
||||||
|
- Views stack as windows in one krate
|
||||||
|
|
||||||
|
## Keyboard Navigation
|
||||||
|
|
||||||
|
| Key | Context | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| ↑ / ↓ | Spotlight | Navigate results |
|
||||||
|
| Tab | Spotlight | Apply type filter / cycle filters |
|
||||||
|
| Backspace | Spotlight (empty query, filter active) | Clear type filter |
|
||||||
|
| Enter | Spotlight (result selected) | Open default view + close |
|
||||||
|
| ⌥L/S/D/Y | Spotlight | Open that view for selected result |
|
||||||
|
| Esc | Spotlight | Close spotlight |
|
||||||
|
|
||||||
|
## Auto-Close Behavior
|
||||||
|
- After 500ms of keyboard idle (no more keypresses)
|
||||||
|
- Spotlight closes automatically
|
||||||
|
- Camera flies to the newly created krate
|
||||||
|
- Debounce timer resets on each keypress
|
||||||
|
|
||||||
|
## Search Results Include
|
||||||
|
- **Existing krates** (jump to working set, camera flies there)
|
||||||
|
- **Namespace results** (`ns/payments`) → opens collection window
|
||||||
|
- **Category results** (`All Pods`, `All Services`) → opens collection window
|
||||||
|
- **Kubernetes objects**: pods, deployments, services, secrets, configmaps, etc.
|
||||||
|
|
||||||
|
## State
|
||||||
|
```typescript
|
||||||
|
interface SpotlightState {
|
||||||
|
open: boolean;
|
||||||
|
query: string;
|
||||||
|
filterType: string | null;
|
||||||
|
sel: number; // selected result index
|
||||||
|
navigated: boolean; // true after ↑/↓ press
|
||||||
|
session: { // tracks views opened before closing
|
||||||
|
kid: string;
|
||||||
|
objId: string;
|
||||||
|
views: string[];
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
1. **Fuzzy search speed**: For real clusters with thousands of objects, debounce by 16ms or run in Web Worker
|
||||||
|
2. **View shortcuts**: Must use `event.code` for layout safety across OS/keyboard layouts
|
||||||
|
3. **Auto-close timer**: Reset on each keypress, don't close during active typing
|
||||||
|
4. **Pre-seeding**: All printable characters (not just letters) should pre-seed spotlight
|
||||||
131
design/top-bar.md
Normal file
131
design/top-bar.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Top Bar Feature Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The top bar is the primary navigation and status bar for the application. It provides quick access to cluster information, user presence, and global actions.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
### Container
|
||||||
|
- Position: `absolute; top: 0; left: 0; right: 0; z-index: 10; height ~56px`
|
||||||
|
- Layout: `display: flex; align-items: center; padding: 13px 18px`
|
||||||
|
- Background: None (transparent over canvas)
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Left Side (left→right)
|
||||||
|
|
||||||
|
#### Logo Pill
|
||||||
|
- **Text**: `krates / yard`
|
||||||
|
- **Styling**:
|
||||||
|
- `background: rgba(14,18,25,.82)`
|
||||||
|
- `border: 1px solid rgba(140,165,200,.18)`
|
||||||
|
- `border-radius: 9px`
|
||||||
|
- `padding: 7px 12px`
|
||||||
|
- `backdrop-filter: blur(6px)`
|
||||||
|
- **Diamond glyph**: `clip-path: polygon(50% 0,100% 50%,50% 100%,0 50%)`
|
||||||
|
- **Color**: Accent (e.g., `#4dd6e8` cyan)
|
||||||
|
|
||||||
|
#### Cluster Pill
|
||||||
|
- Content: Cluster name + green health dot
|
||||||
|
- Health dot:
|
||||||
|
- Color: `#4ad07a`
|
||||||
|
- `box-shadow: 0 0 8px #4ad07a` (glow effect)
|
||||||
|
- Optional: Click to switch clusters (multi-cluster support)
|
||||||
|
|
||||||
|
#### Krate Count Pill
|
||||||
|
- Only shown when krates exist
|
||||||
|
- Content: `N krates` or similar
|
||||||
|
- Accent-colored
|
||||||
|
|
||||||
|
### Right Side
|
||||||
|
|
||||||
|
#### Synced Pill
|
||||||
|
- Content: `synced` text + pulsing accent dot
|
||||||
|
- Dots color: `#4dd6e8` (cyan)
|
||||||
|
- Animation:
|
||||||
|
- Opacity: 50% at 50%
|
||||||
|
- Duration: `1.6s`
|
||||||
|
- Easing: `ease-in-out infinite`
|
||||||
|
- Visual indicator of WebSocket connection status
|
||||||
|
|
||||||
|
#### Admin Button
|
||||||
|
- Content: `◉ admin`
|
||||||
|
- Active state: Accent-outlined (e.g., cyan border)
|
||||||
|
- Inactive state: Muted (gray, subtle)
|
||||||
|
- Toggle: Opens/closes admin drawer
|
||||||
|
|
||||||
|
#### Roster Avatars
|
||||||
|
- Overlapping circles: `margin-left: -7px`
|
||||||
|
- Size: 30×30px
|
||||||
|
- Color: Per-user (from palette)
|
||||||
|
- Content: 2-letter initials
|
||||||
|
- **Self indicator**: `★` (star) instead of initials
|
||||||
|
- Interactive: Click to jump to user's krate (admin mode)
|
||||||
|
|
||||||
|
## Admin Drawer Trigger
|
||||||
|
|
||||||
|
### Button Position
|
||||||
|
- Rightmost interactive element
|
||||||
|
- Click to slide in right-side panel
|
||||||
|
|
||||||
|
### Drawer Features
|
||||||
|
- Width: 380px
|
||||||
|
- Position: `absolute; right: 0; z-index: 56`
|
||||||
|
- Backdrop: `rgba(7,9,13,.4)` (click to close)
|
||||||
|
- Background: `rgba(13,17,24,.98); border-left: 1px solid rgba(140,165,200,.2)`
|
||||||
|
|
||||||
|
### User Card Content
|
||||||
|
For each connected user:
|
||||||
|
- Avatar circle + name
|
||||||
|
- Status: `active · namespace` or `idle Nm`
|
||||||
|
- Current search query + "typing…" indicator (if spotlight open)
|
||||||
|
- Krate count
|
||||||
|
- Clickable krate rows: name + view badges + status dot
|
||||||
|
- **Spectate**: Click user's krate to fly camera to it
|
||||||
|
|
||||||
|
### Self User
|
||||||
|
- Shows real-time state
|
||||||
|
- Displays current spotlight query
|
||||||
|
- Lists actual open krates
|
||||||
|
- Updates live as user interacts
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Key | Context | Action |
|
||||||
|
|---|---|---|
|
||||||
|
| None (top bar is static UI) | - | - |
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### Top Bar References
|
||||||
|
```
|
||||||
|
- clusterName: string
|
||||||
|
- clusterHealthy: boolean
|
||||||
|
- krateCount: number
|
||||||
|
- users: User[]
|
||||||
|
- currentUser: User
|
||||||
|
- adminOpen: boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Object
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
initials: string;
|
||||||
|
self: boolean;
|
||||||
|
status: 'active' | 'idle';
|
||||||
|
namespace?: string;
|
||||||
|
idleMinutes?: number;
|
||||||
|
query?: string;
|
||||||
|
krateIds: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
1. **Admin presence**: Requires CRDT awareness or WebSocket presence channel
|
||||||
|
2. **Roster overflow**: Handle many users gracefully (scroll or truncate with `+N`)
|
||||||
|
3. **Self identification**: Mark current user with star `★`, not just initials
|
||||||
|
4. **Health dot**: Use shadow for glow, not just color
|
||||||
|
5. **Synced animation**: CSS keyframe or React state toggle every 800ms
|
||||||
Reference in New Issue
Block a user