Files
Hermes Agent 78f19cde7d 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
2026-06-16 08:32:47 -04:00

568 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff: Krates — Kubernetes Visual 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