ADR-017-v4: Unified workspace Architecture (Part 2: Technical)
Document: ADR-017-v4-unified-workspace-architecture-part2-technical
Version: 3.0.0
Purpose: Complete technical implementation for unified workspace architecture with GKE StatefulSets
Audience: Developers, AI agents implementing workspace system
Date Created: 2025-08-29
Date Modified: 2025-09-03
QA Reviewed: Pending
Status: UPDATED_FOR_STATEFULSETS
Supersedes: v2.0.0
Changes: Replaced ephemeral containers with GKE StatefulSets
Table of Contents​
- 1. Document Information
- 2. Implementation Overview
- 3. Core State Management
- 4. Activity Bar Implementation
- 5. Sidebar System
- 6. editor Area
- 7. Panel Area
- 8. Logging Patterns
- 9. Error Handling
- 10. Testing Implementation
- 11. Deployment Configuration
- 12. Performance Benchmarks
- 13. Security Implementation
- 14. Migration Scripts
- 15. Operational Runbooks
- 16. Monitoring Setup
- 17. QA Review Block
1. Document Information 🔴 REQUIRED​
| Field | Value |
|---|---|
| ADR Number | ADR-017 |
| Title | Unified workspace Architecture - Technical |
| Status | Updated for StatefulSets |
| Date Created | 2025-08-29 |
| Last Modified | 2025-09-03 |
| Version | 3.0.0 |
| Dependencies | React 18+, Monaco editor 0.44+, WebSocket, FoundationDB, GKE, k8s-openapi |
2. Implementation Overview 🔴 REQUIRED​
2.1 Dependencies​
{
"dependencies": {
"react": "^18.2.0",
"@monaco-editor/react": "^4.6.0",
"zustand": "^4.4.1",
"@tanstack/react-query": "^4.32.6",
"socket.io-client": "^4.7.2",
"react-router-dom": "^6.15.0",
"@emotion/react": "^11.11.1",
"framer-motion": "^10.16.1",
"react-virtual": "^2.10.4",
"idb": "^7.1.1"
}
}
2.2 Module Structure​
// src/workspace/
export { ActivityBar } from './activity-bar';
export { Sidebar } from './sidebar';
export { editorArea } from './editor-area';
export { PanelArea } from './panel-area';
export { StatusBar } from './status-bar';
export { useworkspaceStore } from './store';
3. Core State Management 🔴 REQUIRED​
3.1 Zustand Store Implementation​
// stores/workspace-store.ts
interface workspaceState {
layout: {
activityBar: { visible: boolean; width: number; position: 'left' | 'right' };
sideBar: { visible: boolean; width: number; activeView: string; };
editor: {
groups: editorGroup[];
activeGroupId: string;
splitDirection: 'horizontal' | 'vertical';
};
panel: { visible: boolean; height: number; activeView: string; };
statusBar: { visible: boolean; items: StatusBarItem[]; };
};
// Persistent user preferences
preferences: {
theme: 'light' | 'dark' | 'auto';
fontSize: number;
tabSize: number;
wordWrap: boolean;
minimap: boolean;
shortcuts: Record<string, string>;
};
// Active workspace data
workspace: {
files: FileNode[];
openFiles: OpenFile[];
activeFileId: string | null;
unsavedChanges: Set<string>;
};
}
// Zustand store implementation
export const useworkspaceStore = create<workspaceState>()(
devtools(
persist(
(set, get) => ({
// Initial state
layout: DEFAULT_LAYOUT,
preferences: DEFAULT_PREFERENCES,
workspace: EMPTY_WORKSPACE,
// Actions
toggleActivityBar: () => set((state) => ({
layout: {
...state.layout,
activityBar: {
...state.layout.activityBar,
visible: !state.layout.activityBar.visible
}
}
})),
setSideBarView: (view: string) => set((state) => ({
layout: {
...state.layout,
sideBar: {
...state.layout.sideBar,
activeView: view,
visible: true
}
}
})),
// editor management
openFile: (file: FileNode) => {
const openFile: OpenFile = {
id: file.id,
path: file.path,
content: null, // Loaded async
language: detectLanguage(file.path),
isDirty: false
};
set((state) => ({
workspace: {
...state.workspace,
openFiles: [...state.workspace.openFiles, openFile],
activeFileId: file.id
}
}));
// Async load content
loadFileContent(file.path);
}
}),
{
name: 'coditect-workspace',
partialize: (state) => ({
layout: state.layout,
preferences: state.preferences
})
}
)
)
);
WebSocket Service Architecture​
// services/WebSocketService.ts
export class WebSocketService {
private ws: WebSocket | null = null;
private reconnectTimer: NodeJS.Timeout | null = null;
private subscribers = new Map<string, Set<SubscriptionHandler>>();
private messageQueue: QueuedMessage[] = [];
private reconnectDelay = 1000;
private maxReconnectDelay = 30000;
constructor(private config: WebSocketConfig) {
this.connect();
}
private connect(): void {
const wsUrl = `${this.config.baseUrl}/ws?token=${this.config.token}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
console.log('[WS] Connected');
this.reconnectDelay = 1000;
this.flushMessageQueue();
this.emit('connection', { status: 'connected' });
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onerror = (error) => {
console.error('[WS] Error:', error);
this.emit('connection', { status: 'error', error });
};
this.ws.onclose = () => {
console.log('[WS] Disconnected');
this.emit('connection', { status: 'disconnected' });
this.scheduleReconnect();
};
}
private handleMessage(message: WSMessage): void {
// Route to appropriate handlers
switch (message.type) {
case 'file:changed':
this.emit('file:changed', message.data);
break;
case 'agent:status':
this.emit('agent:status', message.data);
break;
case 'terminal:output':
this.emit('terminal:output', message.data);
break;
case 'log:entry':
this.emit('log:entry', message.data);
break;
case 'task:progress':
this.emit('task:progress', message.data);
break;
default:
console.warn('[WS] Unknown message type:', message.type);
}
}
public subscribe(event: string, handler: SubscriptionHandler): () => void {
if (!this.subscribers.has(event)) {
this.subscribers.set(event, new Set());
}
this.subscribers.get(event)!.add(handler);
// Return unsubscribe function
return () => {
this.subscribers.get(event)?.delete(handler);
};
}
public send(type: string, data: any): void {
const message = { type, data, id: generateId() };
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
this.messageQueue.push(message);
}
}
private emit(event: string, data: any): void {
this.subscribers.get(event)?.forEach(handler => {
try {
handler(data);
} catch (error) {
console.error(`[WS] Handler error for ${event}:`, error);
}
});
}
private scheduleReconnect(): void {
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, this.reconnectDelay);
// Exponential backoff
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
}
private flushMessageQueue(): void {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift()!;
this.ws!.send(JSON.stringify(message));
}
}
}
// React hook for WebSocket
export function useWebSocket() {
const [ws] = useState(() => new WebSocketService(WS_CONFIG));
useEffect(() => {
return () => ws.disconnect();
}, [ws]);
return ws;
}
Component Architecture​
// components/workspace/workspaceProvider.tsx
export const workspaceProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const ws = useWebSocket();
const { theme } = useworkspaceStore(state => state.preferences);
// Global keyboard shortcuts
useHotkeys('cmd+p', () => openQuickOpen());
useHotkeys('cmd+shift+p', () => openCommandPalette());
useHotkeys('cmd+b', () => toggleSidebar());
useHotkeys('cmd+j', () => togglePanel());
// Theme management
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
// WebSocket subscriptions
useEffect(() => {
const unsubscribers = [
ws.subscribe('file:changed', handleFileChange),
ws.subscribe('agent:status', handleAgentStatus),
ws.subscribe('terminal:output', handleterminalOutput),
];
return () => unsubscribers.forEach(unsub => unsub());
}, [ws]);
return (
<workspaceContext.Provider value={{ ws }}>
<ChakraProvider theme={workspaceTheme}>
<ErrorBoundary>
{children}
</ErrorBoundary>
</ChakraProvider>
</workspaceContext.Provider>
);
};
Activity System​
// components/Activities/ActivityRegistry.ts
interface Activity {
id: string;
title: string;
icon: IconType;
shortcut?: string;
component: React.LazyExoticComponent<React.FC>;
position?: number;
badge?: () => string | number | null;
visible?: () => boolean;
}
class ActivityRegistry {
private activities = new Map<string, Activity>();
register(activity: Activity): void {
this.activities.set(activity.id, activity);
}
unregister(id: string): void {
this.activities.delete(id);
}
getAll(): Activity[] {
return Array.from(this.activities.values())
.filter(a => a.visible ? a.visible() : true)
.sort((a, b) => (a.position ?? 999) - (b.position ?? 999));
}
get(id: string): Activity | undefined {
return this.activities.get(id);
}
}
export const activityRegistry = new ActivityRegistry();
// Register core activities
activityRegistry.register({
id: 'explorer',
title: 'Explorer',
icon: FiFolder,
shortcut: 'Ctrl+Shift+E',
component: lazy(() => import('./Explorer/file-explorer')),
position: 1,
});
activityRegistry.register({
id: 'search',
title: 'Search',
icon: FiSearch,
shortcut: 'Ctrl+Shift+F',
component: lazy(() => import('./Search/GlobalSearch')),
position: 2,
});
activityRegistry.register({
id: 'git',
title: 'Source Control',
icon: FiGitBranch,
shortcut: 'Ctrl+Shift+G',
component: lazy(() => import('./Git/GitPanel')),
position: 3,
badge: () => useGitStore(state => state.changes.length),
});
activityRegistry.register({
id: 'agents',
title: 'AI Agents',
icon: FiCpu,
component: lazy(() => import('./Agents/AgentMonitor')),
position: 4,
badge: () => useAgentStore(state => state.activeAgents.length),
});
editor Management​
// components/editor/editorManager.tsx
interface editorTab {
id: string;
uri: string;
title: string;
icon?: IconType;
isDirty: boolean;
viewState?: any;
editor: editorType;
}
type editorType =
| { type: 'monaco'; language: string; }
| { type: 'adr'; mode: 'split' | 'narrative' | 'technical'; }
| { type: 'terminal'; sessionId: string; }
| { type: 'dashboard'; component: string; }
| { type: 'logs'; source: string; };
export const editorManager: React.FC = () => {
const { groups, activeGroupId } = useworkspaceStore(state => state.layout.editor);
const activeGroup = groups.find(g => g.id === activeGroupId);
const rendereditor = (tab: editorTab) => {
switch (tab.editor.type) {
case 'monaco':
return (
<Monacoeditor
key={tab.id}
uri={tab.uri}
language={tab.editor.language}
onChange={(value) => handleeditorChange(tab.id, value)}
options={{
theme: 'coditect-dark',
fontSize: 14,
minimap: { enabled: true },
wordWrap: 'on',
}}
/>
);
case 'adr':
return (
<ADReditor
key={tab.id}
uri={tab.uri}
mode={tab.editor.mode}
onValidation={(score) => updateTabBadge(tab.id, score)}
/>
);
case 'terminal':
return (
<terminal
key={tab.id}
sessionId={tab.editor.sessionId}
onOutput={(data) => handleterminalOutput(tab.id, data)}
/>
);
case 'dashboard':
return (
<DashboardContainer
key={tab.id}
component={tab.editor.component}
/>
);
case 'logs':
return (
<log-viewer
key={tab.id}
source={tab.editor.source}
embedded={true}
/>
);
default:
return <Text>Unknown editor type</Text>;
}
};
return (
<Box h="full" bg="editor.background">
{/* Tab bar */}
<Tabs index={activeGroup?.activeTabIndex} onChange={handleTabChange}>
<TabList>
{activeGroup?.tabs.map(tab => (
<Tab key={tab.id}>
<HStack spacing={2}>
{tab.icon && <Icon as={tab.icon} />}
<Text>{tab.title}</Text>
{tab.isDirty && <Circle size="8px" bg="orange.400" />}
</HStack>
</Tab>
))}
</TabList>
<TabPanels>
{activeGroup?.tabs.map(tab => (
<TabPanel key={tab.id} p={0} h="calc(100vh - 35px)">
<ErrorBoundary fallback={<editorError />}>
{rendereditor(tab)}
</ErrorBoundary>
</TabPanel>
))}
</TabPanels>
</Tabs>
</Box>
);
};
Virtual Scrolling for Logs​
// components/log-viewer/VirtualLogList.tsx
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
interface VirtualLogListProps {
entries: LogEntry[];
onItemClick?: (entry: LogEntry) => void;
}
export const VirtualLogList: React.FC<VirtualLogListProps> = ({
entries,
onItemClick
}) => {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const entry = entries[index];
return (
<Box
style={style}
onClick={() => onItemClick?.(entry)}
_hover={{ bg: 'gray.50' }}
cursor="pointer"
px={4}
borderBottom="1px solid"
borderColor="gray.200"
>
<HStack spacing={4}>
<Badge size="sm" colorScheme={getSeverityColor(entry.severity)}>
{entry.severity}
</Badge>
<Text fontSize="xs" fontFamily="mono" color="gray.600">
{formatTimestamp(entry.timestamp)}
</Text>
<Badge variant="subtle" colorScheme={getActorColor(entry.actor.type)}>
{entry.actor.type}
</Badge>
<Code fontSize="xs">{entry.action}</Code>
<Text flex={1} noOfLines={1}>{entry.message}</Text>
</HStack>
</Box>
);
};
return (
<AutoSizer>
{({ height, width }) => (
<List
height={height}
itemCount={entries.length}
itemSize={40}
width={width}
overscanCount={10}
>
{Row}
</List>
)}
</AutoSizer>
);
};
File System Integration​
// services/FileSystemService.ts
export class FileSystemService {
private fileCache = new Map<string, CachedFile>();
private watchHandlers = new Map<string, WatchHandler[]>();
async readFile(path: string): Promise<string> {
// Check cache first
const cached = this.fileCache.get(path);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.content;
}
// Fetch from server
const response = await api.get(`/files/${encodeURIComponent(path)}`);
const content = response.data.content;
// Update cache
this.fileCache.set(path, {
content,
timestamp: Date.now(),
etag: response.headers.etag
});
return content;
}
async writeFile(path: string, content: string): Promise<void> {
const response = await api.put(`/files/${encodeURIComponent(path)}`, {
content,
etag: this.fileCache.get(path)?.etag
});
// Update cache
this.fileCache.set(path, {
content,
timestamp: Date.now(),
etag: response.headers.etag
});
// Notify watchers
this.notifyWatchers(path, { type: 'changed', path, content });
}
watch(pattern: string, handler: WatchHandler): () => void {
if (!this.watchHandlers.has(pattern)) {
this.watchHandlers.set(pattern, []);
}
this.watchHandlers.get(pattern)!.push(handler);
// Return unwatch function
return () => {
const handlers = this.watchHandlers.get(pattern);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) handlers.splice(index, 1);
}
};
}
private notifyWatchers(path: string, event: FileEvent): void {
for (const [pattern, handlers] of this.watchHandlers) {
if (minimatch(path, pattern)) {
handlers.forEach(handler => {
try {
handler(event);
} catch (error) {
console.error('Watch handler error:', error);
}
});
}
}
}
}
Performance Optimizations​
// hooks/useOptimizedState.ts
export function useOptimizedState<T>(
initialState: T,
options?: { debounce?: number; throttle?: number }
): [T, (value: T | ((prev: T) => T)) => void] {
const [state, setState] = useState(initialState);
const pendingUpdate = useRef<T | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const optimizedSetState = useCallback((value: T | ((prev: T) => T)) => {
if (options?.debounce) {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
pendingUpdate.current = typeof value === 'function'
? value(pendingUpdate.current ?? state)
: value;
timeoutRef.current = setTimeout(() => {
setState(pendingUpdate.current!);
pendingUpdate.current = null;
}, options.debounce);
} else if (options?.throttle) {
if (!timeoutRef.current) {
setState(value);
timeoutRef.current = setTimeout(() => {
timeoutRef.current = null;
if (pendingUpdate.current !== null) {
setState(pendingUpdate.current);
pendingUpdate.current = null;
}
}, options.throttle);
} else {
pendingUpdate.current = typeof value === 'function'
? value(pendingUpdate.current ?? state)
: value;
}
} else {
setState(value);
}
}, [state, options?.debounce, options?.throttle]);
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
return [state, optimizedSetState];
}
// Usage in search
export const GlobalSearch: React.FC = () => {
const [query, setQuery] = useOptimizedState('', { debounce: 300 });
const [results, setResults] = useState<SearchResult[]>([]);
useEffect(() => {
if (query) {
searchFiles(query).then(setResults);
} else {
setResults([]);
}
}, [query]);
return (
<Input
placeholder="Search files..."
onChange={(e) => setQuery(e.target.value)}
/>
);
};
Security Implementation​
// security/SecurityContext.tsx
export const SecurityProvider: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
// Content Security Policy
useEffect(() => {
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"connect-src 'self' wss: https:",
"font-src 'self'",
"worker-src 'self' blob:",
].join('; ');
document.head.appendChild(meta);
return () => document.head.removeChild(meta);
}, []);
// Input sanitization
const sanitizeInput = useCallback((input: string): string => {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: [],
ALLOWED_ATTR: []
});
}, []);
// Command validation for terminal
const validateCommand = useCallback((command: string): boolean => {
const dangerous = [
/rm\s+-rf\s+\//,
/:(){ :|:& };:/,
/>\s*\/dev\/sda/,
/dd\s+if=.*of=\/dev\//
];
return !dangerous.some(pattern => pattern.test(command));
}, []);
return (
<SecurityContext.Provider value={{ sanitizeInput, validateCommand }}>
{children}
</SecurityContext.Provider>
);
};
Testing Strategy​
// __tests__/workspace.integration.test.tsx
describe('workspace Integration', () => {
let mockWs: MockWebSocketServer;
beforeEach(() => {
mockWs = new MockWebSocketServer();
mockWs.start();
});
afterEach(() => {
mockWs.stop();
});
it('should handle file changes from multiple sources', async () => {
const { getByText, getByTestId } = render(
<workspaceProvider>
<Unifiedworkspace />
</workspaceProvider>
);
// Open a file
fireEvent.click(getByText('test.ts'));
await waitFor(() => expect(getByTestId('editor')).toBeInTheDocument());
// Simulate external file change
mockWs.send({
type: 'file:changed',
data: { path: '/workspace/test.ts', content: 'updated content' }
});
// Verify UI updates
await waitFor(() => {
expect(getByTestId('dirty-indicator')).toBeInTheDocument();
expect(getByText('File changed externally')).toBeInTheDocument();
});
});
it('should maintain layout state across sessions', async () => {
const { rerender } = render(<Unifiedworkspace />);
// Change layout
fireEvent.click(screen.getByLabelText('Toggle sidebar'));
fireEvent.drag(screen.getByTestId('panel-resizer'), { y: -100 });
// Unmount and remount
rerender(<div />);
rerender(<Unifiedworkspace />);
// Verify layout persisted
expect(screen.queryByTestId('sidebar')).not.toBeInTheDocument();
expect(screen.getByTestId('panel')).toHaveStyle({ height: '300px' });
});
});
Deployment Configuration​
# k8s/workspace-deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: workspace-service
namespace: coditect
spec:
clusterIP: None
selector:
app: coditect-workspace
ports:
- name: http
port: 8080
targetPort: 8080
- name: websocket
port: 8443
targetPort: 8443
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: workspace-ingress
namespace: coditect
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/websocket-services: "workspace-service"
spec:
tls:
- hosts:
- workspace.coditect.io
secretName: workspace-tls
rules:
- host: workspace.coditect.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: workspace-service
port:
number: 8080
- path: /ws
pathType: Prefix
backend:
service:
name: workspace-service
port:
number: 8443
Performance Benchmarks​
// Minimum performance requirements
export const PERFORMANCE_TARGETS = {
workspaceReady: 5000, // ms - StatefulSet pod startup
initialLoad: 3000, // ms - UI load after pod ready
fileOpen: 100, // ms - from PersistentVolume
searchResponse: 500, // ms
logFilter: 200, // ms for 1M entries
editorLatency: 16, // ms (60fps)
wsReconnect: 1000, // ms
stateRestore: 1000, // ms - from PersistentVolume
memory: {
idle: 100, // MB
active: 500, // MB
max: 2000, // MB
},
storage: {
workspacePVC: 10, // GB
tmpPVC: 5, // GB
iops: 3000, // baseline IOPS
},
metrics: {
fps: 60,
tti: 2000, // Time to Interactive
fcp: 1000, // First Contentful Paint
lcp: 2500, // Largest Contentful Paint
podStartup: 5000, // StatefulSet pod ready
}
};
13. Security Implementation 🔴 REQUIRED​
13.1 Content Security Policy​
// src/security/csp.ts
export const CSP_POLICY = {
'default-src': ["'self'"],
'script-src': ["'self'", "'unsafe-eval'", 'https://cdn.monaco-editor.org'],
'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
'img-src': ["'self'", 'data:', 'https:'],
'connect-src': ["'self'", 'wss:', 'https:'],
'worker-src': ["'self'", 'blob:'],
'font-src': ["'self'", 'https://fonts.gstatic.com'],
};
13.2 Input Sanitization​
// src/security/sanitizer.ts
import DOMPurify from 'dompurify';
export class InputSanitizer {
static sanitizeHTML(input: string): string {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'code', 'pre'],
ALLOWED_ATTR: ['class']
});
}
static sanitizeFilePath(path: string): string {
// Prevent directory traversal
return path.replace(/\.\./g, '').replace(/[<>:"|?*]/g, '');
}
static validateCommand(command: string): boolean {
const ALLOWED_COMMANDS = /^(ls|cat|grep|find|git|npm|cargo|python)(\s|$)/;
return ALLOWED_COMMANDS.test(command);
}
}
14. Migration Scripts 🔴 REQUIRED​
// src/migration/workspace-migration.ts
export class workspaceMigration {
async migrateToV2(): Promise<void> {
const db = await openDB('coditect-workspace', 2, {
upgrade(db, oldVersion) {
if (oldVersion < 2) {
// Migrate layout preferences
const layoutStore = db.createObjectStore('layout', { keyPath: 'id' });
layoutStore.createIndex('userId', 'userId');
// Migrate file cache
const fileStore = db.createObjectStore('files', { keyPath: 'path' });
fileStore.createIndex('lastModified', 'lastModified');
}
},
});
// Migrate existing data
await this.migratelayoutData(db);
await this.migratePreferences(db);
}
}
15. Operational Runbooks 🔴 REQUIRED​
# workspace Emergency Procedures
## High Memory Usage
1. Check browser DevTools memory tab
2. Identify memory leaks in components
3. Force garbage collection
4. Restart workspace if needed
## WebSocket Connection Issues
1. Check network connectivity
2. Verify WebSocket server status
3. Enable fallback polling mode
4. Queue changes for sync when reconnected
## Performance Degradation
1. Profile with React DevTools
2. Check virtual scrolling performance
3. Analyze bundle size
4. Clear IndexedDB cache if corrupted
16. Monitoring Setup 🔴 REQUIRED​
// src/monitoring/workspace-metrics.ts
export class workspaceMetrics {
private static instance: workspaceMetrics;
track(event: string, properties: Record<string, any>): void {
const entry = {
timestamp: Date.now(),
event,
properties,
sessionId: this.getSessionId(),
userId: this.getUserId(),
tenantId: this.getTenantId(),
};
// Send to logging service
this.sendToLoggingService(entry);
}
trackPerformance(operation: string, duration: number): void {
this.track('workspace.performance', {
operation,
duration,
exceeded: duration > PERFORMANCE_TARGETS[operation],
});
}
}
17. StatefulSet workspace Management 🔴 REQUIRED​
17.1 Kubernetes StatefulSet Configuration​
# k8s/workspace-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: coditect-workspace
namespace: coditect
spec:
serviceName: workspace-service
replicas: 1 # One per user workspace
selector:
matchLabels:
app: coditect-workspace
template:
metadata:
labels:
app: coditect-workspace
spec:
nodeSelector:
cloud.google.com/gke-spot: "true" # Use Spot instances
containers:
- name: workspace
image: gcr.io/{{ PROJECT_ID }}/coditect-workspace:latest
ports:
- containerPort: 8080
name: http
- containerPort: 8443
name: websocket
env:
- name: WORKSPACE_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: USER_ID
value: "{{ USER_ID }}"
- name: TENANT_ID
value: "{{ TENANT_ID }}"
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
volumeMounts:
- name: workspace-storage
mountPath: /workspace
- name: tmp
mountPath: /tmp
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: workspace-storage
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard-rwo
resources:
requests:
storage: 10Gi
- metadata:
name: tmp
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard-rwo
resources:
requests:
storage: 5Gi
17.2 workspace Manager Implementation​
// src/workspace/statefulset_manager.rs
use k8s_openapi::api::apps::v1::{StatefulSet, StatefulSetSpec};
use k8s_openapi::api::core::v1::{
Container, ContainerPort, EnvVar, HTTPGetAction,
PersistentVolumeClaim, PersistentVolumeClaimSpec,
PodSpec, PodTemplateSpec, Probe, ResourceRequirements,
VolumeMount
};
use kube::{Api, Client, Resource};
use std::collections::BTreeMap;
pub struct workspaceManager {
k8s_client: Client,
namespace: String,
}
impl workspaceManager {
pub async fn new() -> Result<Self, Error> {
let k8s_client = Client::try_default().await?;
Ok(Self {
k8s_client,
namespace: "coditect".to_string(),
})
}
pub async fn create_workspace(
&self,
user_id: &str,
tenant_id: &str,
workspace_id: &str,
) -> Result<workspaceInfo, Error> {
let statefulset = self.build_statefulset(user_id, tenant_id, workspace_id);
let api: Api<StatefulSet> = Api::namespaced(
self.k8s_client.clone(),
&self.namespace
);
// Create the StatefulSet
let created = api.create(&Default::default(), &statefulset).await?;
// Wait for pod to be ready
self.wait_for_ready(&created.metadata.name.unwrap_or_default()).await?;
Ok(workspaceInfo {
id: workspace_id.to_string(),
user_id: user_id.to_string(),
tenant_id: tenant_id.to_string(),
status: workspaceStatus::Running,
endpoint: self.get_workspace_endpoint(workspace_id),
})
}
fn build_statefulset(
&self,
user_id: &str,
tenant_id: &str,
workspace_id: &str,
) -> StatefulSet {
let name = format!("workspace-{}", workspace_id);
StatefulSet {
metadata: ObjectMeta {
name: Some(name.clone()),
namespace: Some(self.namespace.clone()),
labels: Some(BTreeMap::from([
("app".to_string(), "coditect-workspace".to_string()),
("user-id".to_string(), user_id.to_string()),
("tenant-id".to_string(), tenant_id.to_string()),
])),
..Default::default()
},
spec: Some(StatefulSetSpec {
service_name: "workspace-service".to_string(),
replicas: Some(1),
selector: LabelSelector {
match_labels: Some(BTreeMap::from([
("app".to_string(), "coditect-workspace".to_string()),
("workspace-id".to_string(), workspace_id.to_string()),
])),
..Default::default()
},
template: PodTemplateSpec {
metadata: Some(ObjectMeta {
labels: Some(BTreeMap::from([
("app".to_string(), "coditect-workspace".to_string()),
("workspace-id".to_string(), workspace_id.to_string()),
])),
..Default::default()
}),
spec: Some(PodSpec {
node_selector: Some(BTreeMap::from([
("cloud.google.com/gke-spot".to_string(), "true".to_string()),
])),
containers: vec![self.build_container(user_id, tenant_id, workspace_id)],
..Default::default()
}),
},
volume_claim_templates: Some(vec![
self.build_workspace_pvc(),
self.build_tmp_pvc(),
]),
..Default::default()
}),
..Default::default()
}
}
fn build_container(
&self,
user_id: &str,
tenant_id: &str,
workspace_id: &str,
) -> Container {
Container {
name: "workspace".to_string(),
image: Some(format!("gcr.io/{}/coditect-workspace:latest",
std::env::var("PROJECT_ID").unwrap_or_default())),
ports: Some(vec![
ContainerPort {
container_port: 8080,
name: Some("http".to_string()),
..Default::default()
},
ContainerPort {
container_port: 8443,
name: Some("websocket".to_string()),
..Default::default()
},
]),
env: Some(vec![
EnvVar {
name: "WORKSPACE_ID".to_string(),
value: Some(workspace_id.to_string()),
..Default::default()
},
EnvVar {
name: "USER_ID".to_string(),
value: Some(user_id.to_string()),
..Default::default()
},
EnvVar {
name: "TENANT_ID".to_string(),
value: Some(tenant_id.to_string()),
..Default::default()
},
]),
resources: Some(ResourceRequirements {
requests: Some(BTreeMap::from([
("cpu".to_string(), Quantity("1".to_string())),
("memory".to_string(), Quantity("2Gi".to_string())),
])),
limits: Some(BTreeMap::from([
("cpu".to_string(), Quantity("2".to_string())),
("memory".to_string(), Quantity("4Gi".to_string())),
])),
}),
volume_mounts: Some(vec![
VolumeMount {
name: "workspace-storage".to_string(),
mount_path: "/workspace".to_string(),
..Default::default()
},
VolumeMount {
name: "tmp".to_string(),
mount_path: "/tmp".to_string(),
..Default::default()
},
]),
liveness_probe: Some(Probe {
http_get: Some(HTTPGetAction {
path: Some("/health".to_string()),
port: IntOrString::Int(8080),
..Default::default()
}),
initial_delay_seconds: Some(30),
period_seconds: Some(10),
..Default::default()
}),
readiness_probe: Some(Probe {
http_get: Some(HTTPGetAction {
path: Some("/ready".to_string()),
port: IntOrString::Int(8080),
..Default::default()
}),
initial_delay_seconds: Some(5),
period_seconds: Some(5),
..Default::default()
}),
..Default::default()
}
}
fn build_workspace_pvc(&self) -> PersistentVolumeClaim {
PersistentVolumeClaim {
metadata: ObjectMeta {
name: Some("workspace-storage".to_string()),
..Default::default()
},
spec: Some(PersistentVolumeClaimSpec {
access_modes: Some(vec!["ReadWriteOnce".to_string()]),
storage_class_name: Some("standard-rwo".to_string()),
resources: Some(ResourceRequirements {
requests: Some(BTreeMap::from([
("storage".to_string(), Quantity("10Gi".to_string())),
])),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}
}
fn build_tmp_pvc(&self) -> PersistentVolumeClaim {
PersistentVolumeClaim {
metadata: ObjectMeta {
name: Some("tmp".to_string()),
..Default::default()
},
spec: Some(PersistentVolumeClaimSpec {
access_modes: Some(vec!["ReadWriteOnce".to_string()]),
storage_class_name: Some("standard-rwo".to_string()),
resources: Some(ResourceRequirements {
requests: Some(BTreeMap::from([
("storage".to_string(), Quantity("5Gi".to_string())),
])),
..Default::default()
}),
..Default::default()
}),
..Default::default()
}
}
async fn wait_for_ready(&self, name: &str) -> Result<(), Error> {
let api: Api<Pod> = Api::namespaced(self.k8s_client.clone(), &self.namespace);
let timeout = Duration::from_secs(300); // 5 minutes
let start = Instant::now();
loop {
if start.elapsed() > timeout {
return Err(Error::Timeout("Pod failed to become ready".to_string()));
}
match api.get(name).await {
Ok(pod) => {
if let Some(status) = &pod.status {
if let Some(conditions) = &status.conditions {
if conditions.iter().any(|c|
c.type_ == "Ready" && c.status == "True"
) {
return Ok(());
}
}
}
}
Err(_) => {} // Pod might not exist yet
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
}
}
17.3 Frontend Integration with StatefulSet workspaces​
// src/services/workspaceService.ts
export class workspaceService {
private baseUrl: string;
constructor() {
this.baseUrl = import.meta.env.VITE_API_URL;
}
async createworkspace(userId: string, tenantId: string): Promise<workspaceInfo> {
const response = await fetch(`${this.baseUrl}/workspaces`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({ userId, tenantId }),
});
if (!response.ok) {
throw new Error('Failed to create workspace');
}
const workspace = await response.json();
// Wait for StatefulSet to be ready
await this.waitForworkspaceReady(workspace.id);
return workspace;
}
async waitForworkspaceReady(workspaceId: string): Promise<void> {
const maxAttempts = 60; // 5 minutes with 5-second intervals
let attempts = 0;
while (attempts < maxAttempts) {
const status = await this.getworkspaceStatus(workspaceId);
if (status.ready) {
return;
}
await new Promise(resolve => setTimeout(resolve, 5000));
attempts++;
}
throw new Error('workspace failed to become ready');
}
async connectToworkspace(workspaceId: string): Promise<WebSocket> {
const wsUrl = `${this.baseUrl.replace('http', 'ws')}/workspaces/${workspaceId}/connect`;
const ws = new WebSocket(wsUrl);
return new Promise((resolve, reject) => {
ws.onopen = () => resolve(ws);
ws.onerror = (error) => reject(error);
});
}
}
17.4 Persistent Volume Management​
// src/workspace/PersistentStorage.ts
export class PersistentStorage {
private workspaceId: string;
private storageApi: string;
constructor(workspaceId: string) {
this.workspaceId = workspaceId;
this.storageApi = `/api/workspaces/${workspaceId}/storage`;
}
async saveworkspaceState(state: workspaceState): Promise<void> {
await fetch(`${this.storageApi}/state`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`,
},
body: JSON.stringify(state),
});
}
async restoreworkspaceState(): Promise<workspaceState | null> {
const response = await fetch(`${this.storageApi}/state`, {
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
},
});
if (!response.ok) {
return null;
}
return response.json();
}
async listFiles(path: string = '/'): Promise<FileNode[]> {
const response = await fetch(`${this.storageApi}/files?path=${encodeURIComponent(path)}`, {
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
},
});
return response.json();
}
}
18. QA Review Block​
Status: AWAITING INDEPENDENT QA REVIEW
This section will be completed by an independent QA reviewer according to ADR-QA-REVIEW-GUIDE-v4.2.
Document ready for review as of: 2025-09-03
Version ready for review: 3.0.0
Changes in v3.0.0​
- Added complete StatefulSet workspace management implementation (Section 17)
- Replaced docker-compose deployment with Kubernetes configurations
- Updated performance benchmarks to include StatefulSet pod startup times
- Added PersistentVolumeClaim specifications for workspace persistence
- Implemented workspaceManager in Rust using k8s-openapi and kube crates
- Added frontend integration for StatefulSet workspace lifecycle
- Included GKE Spot instance configuration for cost optimization
- Modified by: DOCUMENT-DEV-2 (SESSION14) on 2025-09-03