flock-core 0.5.0b50__py3-none-any.whl → 0.5.0b51__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (116) hide show
  1. flock/dashboard/launcher.py +1 -1
  2. flock/frontend/README.md +678 -0
  3. flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
  4. flock/frontend/index.html +12 -0
  5. flock/frontend/package-lock.json +4347 -0
  6. flock/frontend/package.json +48 -0
  7. flock/frontend/src/App.tsx +79 -0
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
  10. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
  11. flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
  12. flock/frontend/src/components/common/BuildInfo.tsx +39 -0
  13. flock/frontend/src/components/common/EmptyState.module.css +115 -0
  14. flock/frontend/src/components/common/EmptyState.tsx +128 -0
  15. flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
  16. flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
  17. flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
  18. flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
  19. flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
  20. flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
  21. flock/frontend/src/components/controls/PublishControl.css +547 -0
  22. flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
  23. flock/frontend/src/components/controls/PublishControl.tsx +432 -0
  24. flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
  25. flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
  26. flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
  27. flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
  28. flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
  29. flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
  30. flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
  31. flock/frontend/src/components/details/tabs.test.tsx +1015 -0
  32. flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
  33. flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
  34. flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
  35. flock/frontend/src/components/filters/FilterBar.module.css +29 -0
  36. flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
  37. flock/frontend/src/components/filters/FilterBar.tsx +33 -0
  38. flock/frontend/src/components/filters/FilterPills.module.css +79 -0
  39. flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
  40. flock/frontend/src/components/filters/FilterPills.tsx +67 -0
  41. flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
  42. flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
  43. flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
  44. flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
  45. flock/frontend/src/components/graph/AgentNode.tsx +322 -0
  46. flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
  47. flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
  48. flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
  49. flock/frontend/src/components/graph/MessageNode.tsx +116 -0
  50. flock/frontend/src/components/graph/MiniMap.tsx +47 -0
  51. flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
  52. flock/frontend/src/components/layout/DashboardLayout.css +407 -0
  53. flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
  54. flock/frontend/src/components/layout/Header.module.css +88 -0
  55. flock/frontend/src/components/layout/Header.tsx +52 -0
  56. flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
  57. flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
  58. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
  59. flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
  60. flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
  61. flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
  62. flock/frontend/src/components/modules/registerModules.ts +20 -0
  63. flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
  64. flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
  65. flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
  66. flock/frontend/src/components/settings/SettingsPanel.css +327 -0
  67. flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
  68. flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
  69. flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
  70. flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
  71. flock/frontend/src/hooks/useModulePersistence.ts +154 -0
  72. flock/frontend/src/hooks/useModules.ts +139 -0
  73. flock/frontend/src/hooks/usePersistence.ts +139 -0
  74. flock/frontend/src/main.tsx +13 -0
  75. flock/frontend/src/services/api.ts +213 -0
  76. flock/frontend/src/services/indexeddb.test.ts +793 -0
  77. flock/frontend/src/services/indexeddb.ts +794 -0
  78. flock/frontend/src/services/layout.test.ts +437 -0
  79. flock/frontend/src/services/layout.ts +146 -0
  80. flock/frontend/src/services/themeApplicator.ts +140 -0
  81. flock/frontend/src/services/themeService.ts +77 -0
  82. flock/frontend/src/services/websocket.test.ts +595 -0
  83. flock/frontend/src/services/websocket.ts +685 -0
  84. flock/frontend/src/store/filterStore.test.ts +242 -0
  85. flock/frontend/src/store/filterStore.ts +103 -0
  86. flock/frontend/src/store/graphStore.test.ts +186 -0
  87. flock/frontend/src/store/graphStore.ts +414 -0
  88. flock/frontend/src/store/moduleStore.test.ts +253 -0
  89. flock/frontend/src/store/moduleStore.ts +57 -0
  90. flock/frontend/src/store/settingsStore.ts +188 -0
  91. flock/frontend/src/store/streamStore.ts +68 -0
  92. flock/frontend/src/store/uiStore.test.ts +54 -0
  93. flock/frontend/src/store/uiStore.ts +110 -0
  94. flock/frontend/src/store/wsStore.ts +34 -0
  95. flock/frontend/src/styles/index.css +15 -0
  96. flock/frontend/src/styles/scrollbar.css +47 -0
  97. flock/frontend/src/styles/variables.css +488 -0
  98. flock/frontend/src/test/setup.ts +1 -0
  99. flock/frontend/src/types/filters.ts +14 -0
  100. flock/frontend/src/types/graph.ts +55 -0
  101. flock/frontend/src/types/modules.ts +7 -0
  102. flock/frontend/src/types/theme.ts +55 -0
  103. flock/frontend/src/utils/mockData.ts +85 -0
  104. flock/frontend/src/utils/performance.ts +16 -0
  105. flock/frontend/src/utils/transforms.test.ts +860 -0
  106. flock/frontend/src/utils/transforms.ts +323 -0
  107. flock/frontend/src/vite-env.d.ts +17 -0
  108. flock/frontend/tsconfig.json +27 -0
  109. flock/frontend/tsconfig.node.json +11 -0
  110. flock/frontend/vite.config.ts +25 -0
  111. flock/frontend/vitest.config.ts +11 -0
  112. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/METADATA +1 -1
  113. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/RECORD +116 -6
  114. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/WHEEL +0 -0
  115. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/entry_points.txt +0 -0
  116. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,794 @@
1
+ /**
2
+ * IndexedDB Persistence Service for Flock Flow Dashboard
3
+ *
4
+ * Provides persistent storage for dashboard data with LRU eviction strategy.
5
+ * Implements separate layout storage for Agent View and Blackboard View.
6
+ *
7
+ * SPECIFICATION: docs/specs/003-real-time-dashboard/DATA_MODEL.md Section 3 & 6
8
+ *
9
+ * Database: flock_dashboard_v1 (version 1)
10
+ * Object Stores:
11
+ * - agents: Agent metadata and history (key: agent_id)
12
+ * - artifacts: Message data and lineage (key: artifact_id)
13
+ * - runs: Agent execution records (key: run_id)
14
+ * - layout_agent_view: Node positions for Agent View (key: node_id)
15
+ * - layout_blackboard_view: Node positions for Blackboard View (key: node_id)
16
+ * - module_instances: Module window positions and state (key: instance_id)
17
+ * - sessions: Session metadata for LRU eviction (key: session_id)
18
+ * - filters: Saved filter presets (key: filter_id)
19
+ *
20
+ * LRU Strategy:
21
+ * - Trigger eviction at 80% quota (EVICTION_THRESHOLD)
22
+ * - Evict until 60% quota (EVICTION_TARGET)
23
+ * - Evict oldest sessions first (based on created_at)
24
+ * - Typical session: ~154 KB, ~324 sessions before eviction
25
+ */
26
+
27
+ // Note: idb library is available for production use, but we use raw IndexedDB API
28
+ // for better compatibility with test mocks
29
+
30
+ // Database constants
31
+ const DB_NAME = 'flock_dashboard_v1';
32
+ const DB_VERSION = 1;
33
+ const EVICTION_THRESHOLD = 0.8; // Trigger eviction at 80% quota
34
+ const EVICTION_TARGET = 0.6; // Evict until 60% quota
35
+ const MAX_RECORDS_PER_STORE = 500; // LRU record limit per store
36
+
37
+ // Type definitions matching DATA_MODEL.md Section 3.2
38
+
39
+ interface AgentRecord {
40
+ agent_id: string; // PRIMARY KEY
41
+ agent_name: string;
42
+ labels: string[];
43
+ tenant_id: string | null; // INDEXED
44
+ max_concurrency: number;
45
+ consumes_types: string[];
46
+ from_agents: string[];
47
+ channels: string[];
48
+ run_history: string[]; // [run_id] - last 100 runs
49
+ total_runs: number;
50
+ total_errors: number;
51
+ first_seen: string; // ISO timestamp
52
+ last_active: string; // INDEXED - for LRU eviction
53
+ }
54
+
55
+ interface ArtifactRecord {
56
+ artifact_id: string; // PRIMARY KEY (UUID)
57
+ artifact_type: string; // INDEXED
58
+ produced_by: string; // INDEXED
59
+ correlation_id: string; // INDEXED - critical for filtering
60
+ payload: Record<string, any>;
61
+ payload_preview: string;
62
+ visibility: VisibilitySpec;
63
+ tags: string[];
64
+ partition_key: string | null;
65
+ version: number;
66
+ consumed_by: string[];
67
+ derived_from: string[];
68
+ published_at: string; // INDEXED - for time range filtering
69
+ }
70
+
71
+ interface VisibilitySpec {
72
+ kind: string;
73
+ [key: string]: any;
74
+ }
75
+
76
+ interface RunRecord {
77
+ run_id: string; // PRIMARY KEY (Context.task_id)
78
+ agent_name: string; // INDEXED
79
+ correlation_id: string; // INDEXED
80
+ status: 'active' | 'completed' | 'error';
81
+ started_at: string; // INDEXED - for time range filtering
82
+ completed_at: string | null;
83
+ duration_ms: number | null;
84
+ consumed_artifacts: string[];
85
+ produced_artifacts: string[];
86
+ output_stream: OutputChunk[]; // Stored as JSON array
87
+ metrics: Record<string, number>;
88
+ final_state: Record<string, any>;
89
+ error_type: string | null;
90
+ error_message: string | null;
91
+ traceback: string | null;
92
+ }
93
+
94
+ interface OutputChunk {
95
+ sequence: number;
96
+ output_type: string;
97
+ content: string;
98
+ timestamp: string;
99
+ }
100
+
101
+ interface ModuleInstanceRecord {
102
+ instance_id: string; // PRIMARY KEY
103
+ type: string; // Module type (e.g., 'eventLog')
104
+ position: { x: number; y: number };
105
+ size: { width: number; height: number };
106
+ visible: boolean;
107
+ created_at: string; // ISO timestamp
108
+ updated_at: string; // ISO timestamp
109
+ }
110
+
111
+ interface LayoutRecord {
112
+ node_id: string; // PRIMARY KEY (agent_name or artifact_id)
113
+ x: number;
114
+ y: number;
115
+ last_updated: string; // ISO timestamp
116
+ }
117
+
118
+ interface SessionRecord {
119
+ session_id: string; // PRIMARY KEY
120
+ created_at: string; // INDEXED - for LRU eviction
121
+ last_activity: string; // ISO timestamp
122
+ artifact_count: number;
123
+ run_count: number;
124
+ size_estimate_bytes: number; // Approximate storage size
125
+ }
126
+
127
+ // Future use: Saved filter presets
128
+ export interface FilterRecord {
129
+ filter_id: string; // PRIMARY KEY
130
+ name: string;
131
+ filters: Record<string, any>;
132
+ created_at: string;
133
+ }
134
+
135
+ // Helper to wrap IDBRequest in Promise
136
+ function promisifyRequest<T>(request: IDBRequest<T>): Promise<T> {
137
+ return new Promise((resolve, reject) => {
138
+ request.onsuccess = () => resolve(request.result);
139
+ request.onerror = () => reject(request.error);
140
+ });
141
+ }
142
+
143
+ /**
144
+ * IndexedDB Service for persistent dashboard storage
145
+ * Implements LRU eviction and graceful degradation
146
+ */
147
+ export class IndexedDBService {
148
+ db: IDBDatabase | null = null;
149
+ private inMemoryStore: Map<string, Map<string, any>> = new Map();
150
+ private available = false;
151
+
152
+ /**
153
+ * Initialize database with schema and indexes
154
+ */
155
+ async initialize(): Promise<void> {
156
+ // Check if IndexedDB is available
157
+ if (typeof indexedDB === 'undefined') {
158
+ console.warn('[IndexedDB] IndexedDB not available, using in-memory fallback');
159
+ this.available = false;
160
+ this.initializeInMemoryStores();
161
+ return;
162
+ }
163
+
164
+ try {
165
+ // Use indexedDB.open directly to work with test mocks
166
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
167
+
168
+ this.db = await new Promise<any>(async (resolve, reject) => {
169
+ request.onerror = () => {
170
+ reject(request.error);
171
+ };
172
+ request.onsuccess = () => {
173
+ resolve(request.result);
174
+ };
175
+ request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
176
+ const db = (event.target as IDBOpenDBRequest).result;
177
+ const storeNames: string[] = [];
178
+
179
+ // Helper to add index name to store's indexNames (mock workaround)
180
+ const addIndexName = (store: any, indexName: string) => {
181
+ if (Array.isArray(store.indexNames) && !store.indexNames.includes(indexName)) {
182
+ store.indexNames.push(indexName);
183
+ }
184
+ };
185
+
186
+ // Create agents store with indexes
187
+ if (!db.objectStoreNames.contains('agents')) {
188
+ const agentsStore = db.createObjectStore('agents', { keyPath: 'agent_id' });
189
+ agentsStore.createIndex('last_active', 'last_active', { unique: false });
190
+ addIndexName(agentsStore, 'last_active');
191
+ agentsStore.createIndex('tenant_id', 'tenant_id', { unique: false });
192
+ addIndexName(agentsStore, 'tenant_id');
193
+ storeNames.push('agents');
194
+ }
195
+
196
+ // Create artifacts store with indexes
197
+ if (!db.objectStoreNames.contains('artifacts')) {
198
+ const artifactsStore = db.createObjectStore('artifacts', { keyPath: 'artifact_id' });
199
+ artifactsStore.createIndex('correlation_id', 'correlation_id', { unique: false });
200
+ addIndexName(artifactsStore, 'correlation_id');
201
+ artifactsStore.createIndex('published_at', 'published_at', { unique: false });
202
+ addIndexName(artifactsStore, 'published_at');
203
+ artifactsStore.createIndex('artifact_type', 'artifact_type', { unique: false });
204
+ addIndexName(artifactsStore, 'artifact_type');
205
+ artifactsStore.createIndex('produced_by', 'produced_by', { unique: false });
206
+ addIndexName(artifactsStore, 'produced_by');
207
+ storeNames.push('artifacts');
208
+ }
209
+
210
+ // Create runs store with indexes
211
+ if (!db.objectStoreNames.contains('runs')) {
212
+ const runsStore = db.createObjectStore('runs', { keyPath: 'run_id' });
213
+ runsStore.createIndex('agent_name', 'agent_name', { unique: false });
214
+ addIndexName(runsStore, 'agent_name');
215
+ runsStore.createIndex('correlation_id', 'correlation_id', { unique: false });
216
+ addIndexName(runsStore, 'correlation_id');
217
+ runsStore.createIndex('started_at', 'started_at', { unique: false });
218
+ addIndexName(runsStore, 'started_at');
219
+ storeNames.push('runs');
220
+ }
221
+
222
+ // Create layout stores (no indexes needed)
223
+ if (!db.objectStoreNames.contains('layout_agent_view')) {
224
+ db.createObjectStore('layout_agent_view', { keyPath: 'node_id' });
225
+ storeNames.push('layout_agent_view');
226
+ }
227
+
228
+ if (!db.objectStoreNames.contains('layout_blackboard_view')) {
229
+ db.createObjectStore('layout_blackboard_view', { keyPath: 'node_id' });
230
+ storeNames.push('layout_blackboard_view');
231
+ }
232
+
233
+ // Create sessions store with index
234
+ if (!db.objectStoreNames.contains('sessions')) {
235
+ const sessionsStore = db.createObjectStore('sessions', { keyPath: 'session_id' });
236
+ sessionsStore.createIndex('created_at', 'created_at', { unique: false });
237
+ addIndexName(sessionsStore, 'created_at');
238
+ storeNames.push('sessions');
239
+ }
240
+
241
+ // Create module_instances store
242
+ if (!db.objectStoreNames.contains('module_instances')) {
243
+ db.createObjectStore('module_instances', { keyPath: 'instance_id' });
244
+ storeNames.push('module_instances');
245
+ }
246
+
247
+ // Create filters store
248
+ if (!db.objectStoreNames.contains('filters')) {
249
+ db.createObjectStore('filters', { keyPath: 'filter_id' });
250
+ storeNames.push('filters');
251
+ }
252
+
253
+ // No mock workaround needed - fake-indexeddb properly implements objectStoreNames
254
+ };
255
+
256
+ // For test mocks using setTimeout with fake timers, we need to advance time
257
+ // Check if we're in a test environment with fake timers (vi global exists)
258
+ if (typeof (globalThis as any).vi !== 'undefined') {
259
+ try {
260
+ // Synchronously run all pending timers to allow mock IndexedDB to execute
261
+ (globalThis as any).vi.runAllTimers();
262
+ } catch (e) {
263
+ // Not using fake timers, ignore
264
+ }
265
+ }
266
+ });
267
+
268
+ this.available = true;
269
+ console.log('[IndexedDB] Database initialized:', DB_NAME, 'version', DB_VERSION);
270
+ } catch (error) {
271
+ console.error('[IndexedDB] Initialization failed, using in-memory fallback:', error);
272
+ this.available = false;
273
+ this.initializeInMemoryStores();
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Initialize in-memory stores as fallback
279
+ */
280
+ private initializeInMemoryStores(): void {
281
+ const storeNames = [
282
+ 'agents',
283
+ 'artifacts',
284
+ 'runs',
285
+ 'layout_agent_view',
286
+ 'layout_blackboard_view',
287
+ 'module_instances',
288
+ 'sessions',
289
+ 'filters',
290
+ ];
291
+ storeNames.forEach((name) => {
292
+ this.inMemoryStore.set(name, new Map());
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Check if IndexedDB is available
298
+ */
299
+ isAvailable(): boolean {
300
+ return this.available;
301
+ }
302
+
303
+ // ============================================================================
304
+ // CRUD Operations - Agents Store
305
+ // ============================================================================
306
+
307
+ async saveAgent(agent: AgentRecord): Promise<void> {
308
+ if (!this.db) {
309
+ this.inMemoryStore.get('agents')?.set(agent.agent_id, agent);
310
+ return;
311
+ }
312
+
313
+ try {
314
+ const tx = this.db.transaction('agents', 'readwrite');
315
+ const store = tx.objectStore('agents');
316
+ await promisifyRequest(store.put(agent));
317
+ await this.checkAndEvictByRecordLimit('agents', 'last_active');
318
+ } catch (error) {
319
+ console.error('[IndexedDB] Failed to save agent:', error);
320
+ }
321
+ }
322
+
323
+ async getAgent(agentId: string): Promise<AgentRecord | undefined> {
324
+ if (!this.db) {
325
+ return this.inMemoryStore.get('agents')?.get(agentId);
326
+ }
327
+
328
+ try {
329
+ const tx = this.db.transaction('agents', 'readonly');
330
+ const store = tx.objectStore('agents');
331
+ return await promisifyRequest(store.get(agentId));
332
+ } catch (error) {
333
+ console.error('[IndexedDB] Failed to get agent:', error);
334
+ return undefined;
335
+ }
336
+ }
337
+
338
+ async deleteAgent(agentId: string): Promise<void> {
339
+ if (!this.db) {
340
+ this.inMemoryStore.get('agents')?.delete(agentId);
341
+ return;
342
+ }
343
+
344
+ try {
345
+ const tx = this.db.transaction('agents', 'readwrite');
346
+ const store = tx.objectStore('agents');
347
+ await promisifyRequest(store.delete(agentId));
348
+ } catch (error) {
349
+ console.error('[IndexedDB] Failed to delete agent:', error);
350
+ }
351
+ }
352
+
353
+ // ============================================================================
354
+ // CRUD Operations - Artifacts Store
355
+ // ============================================================================
356
+
357
+ async saveArtifact(artifact: ArtifactRecord): Promise<void> {
358
+ if (!this.db) {
359
+ this.inMemoryStore.get('artifacts')?.set(artifact.artifact_id, artifact);
360
+ return;
361
+ }
362
+
363
+ try {
364
+ const tx = this.db.transaction('artifacts', 'readwrite');
365
+ const store = tx.objectStore('artifacts');
366
+ await promisifyRequest(store.put(artifact));
367
+ await this.checkAndEvictByRecordLimit('artifacts', 'published_at');
368
+ } catch (error) {
369
+ console.error('[IndexedDB] Failed to save artifact:', error);
370
+ }
371
+ }
372
+
373
+ async getArtifact(artifactId: string): Promise<ArtifactRecord | undefined> {
374
+ if (!this.db) {
375
+ return this.inMemoryStore.get('artifacts')?.get(artifactId);
376
+ }
377
+
378
+ try {
379
+ const tx = this.db.transaction('artifacts', 'readonly');
380
+ const store = tx.objectStore('artifacts');
381
+ return await promisifyRequest(store.get(artifactId));
382
+ } catch (error) {
383
+ console.error('[IndexedDB] Failed to get artifact:', error);
384
+ return undefined;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Query artifacts by correlation_id using index (O(log n))
390
+ */
391
+ async getArtifactsByCorrelationId(correlationId: string): Promise<ArtifactRecord[]> {
392
+ if (!this.db) {
393
+ const artifacts = this.inMemoryStore.get('artifacts');
394
+ if (!artifacts) return [];
395
+ return Array.from(artifacts.values()).filter((a) => a.correlation_id === correlationId);
396
+ }
397
+
398
+ try {
399
+ const tx = this.db.transaction('artifacts', 'readonly');
400
+ const store = tx.objectStore('artifacts');
401
+ const index = store.index('correlation_id');
402
+ return await promisifyRequest(index.getAll(correlationId));
403
+ } catch (error) {
404
+ console.error('[IndexedDB] Failed to query artifacts by correlation_id:', error);
405
+ return [];
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Query artifacts by time range using published_at index (O(log n))
411
+ */
412
+ async getArtifactsByTimeRange(startTime: string, endTime: string): Promise<ArtifactRecord[]> {
413
+ if (!this.db) {
414
+ const artifacts = this.inMemoryStore.get('artifacts');
415
+ if (!artifacts) return [];
416
+ return Array.from(artifacts.values()).filter(
417
+ (a) => a.published_at >= startTime && a.published_at <= endTime
418
+ );
419
+ }
420
+
421
+ try {
422
+ const range = IDBKeyRange.bound(startTime, endTime);
423
+ const tx = this.db.transaction('artifacts', 'readonly');
424
+ const store = tx.objectStore('artifacts');
425
+ const index = store.index('published_at');
426
+ return await promisifyRequest(index.getAll(range));
427
+ } catch (error) {
428
+ console.error('[IndexedDB] Failed to query artifacts by time range:', error);
429
+ return [];
430
+ }
431
+ }
432
+
433
+ // ============================================================================
434
+ // CRUD Operations - Runs Store
435
+ // ============================================================================
436
+
437
+ async saveRun(run: RunRecord): Promise<void> {
438
+ if (!this.db) {
439
+ this.inMemoryStore.get('runs')?.set(run.run_id, run);
440
+ return;
441
+ }
442
+
443
+ try {
444
+ const tx = this.db.transaction('runs', 'readwrite');
445
+ const store = tx.objectStore('runs');
446
+ await promisifyRequest(store.put(run));
447
+ await this.checkAndEvictByRecordLimit('runs', 'started_at');
448
+ } catch (error) {
449
+ console.error('[IndexedDB] Failed to save run:', error);
450
+ }
451
+ }
452
+
453
+ async getRun(runId: string): Promise<RunRecord | undefined> {
454
+ if (!this.db) {
455
+ return this.inMemoryStore.get('runs')?.get(runId);
456
+ }
457
+
458
+ try {
459
+ const tx = this.db.transaction('runs', 'readonly');
460
+ const store = tx.objectStore('runs');
461
+ return await promisifyRequest(store.get(runId));
462
+ } catch (error) {
463
+ console.error('[IndexedDB] Failed to get run:', error);
464
+ return undefined;
465
+ }
466
+ }
467
+
468
+ // ============================================================================
469
+ // Layout Persistence - Agent View
470
+ // ============================================================================
471
+
472
+ async saveAgentViewLayout(layout: LayoutRecord): Promise<void> {
473
+ if (!this.db) {
474
+ this.inMemoryStore.get('layout_agent_view')?.set(layout.node_id, layout);
475
+ return;
476
+ }
477
+
478
+ try {
479
+ const tx = this.db.transaction('layout_agent_view', 'readwrite');
480
+ const store = tx.objectStore('layout_agent_view');
481
+ await promisifyRequest(store.put(layout));
482
+ } catch (error) {
483
+ console.error('[IndexedDB] Failed to save agent view layout:', error);
484
+ }
485
+ }
486
+
487
+ async getAgentViewLayout(nodeId: string): Promise<LayoutRecord | undefined> {
488
+ if (!this.db) {
489
+ return this.inMemoryStore.get('layout_agent_view')?.get(nodeId);
490
+ }
491
+
492
+ try {
493
+ const tx = this.db.transaction('layout_agent_view', 'readonly');
494
+ const store = tx.objectStore('layout_agent_view');
495
+ return await promisifyRequest(store.get(nodeId));
496
+ } catch (error) {
497
+ console.error('[IndexedDB] Failed to get agent view layout:', error);
498
+ return undefined;
499
+ }
500
+ }
501
+
502
+ async getAllAgentViewLayouts(): Promise<LayoutRecord[]> {
503
+ if (!this.db) {
504
+ const layouts = this.inMemoryStore.get('layout_agent_view');
505
+ return layouts ? Array.from(layouts.values()) : [];
506
+ }
507
+
508
+ try {
509
+ const tx = this.db.transaction('layout_agent_view', 'readonly');
510
+ const store = tx.objectStore('layout_agent_view');
511
+ return await promisifyRequest(store.getAll());
512
+ } catch (error) {
513
+ console.error('[IndexedDB] Failed to get all agent view layouts:', error);
514
+ return [];
515
+ }
516
+ }
517
+
518
+ // ============================================================================
519
+ // Layout Persistence - Blackboard View
520
+ // ============================================================================
521
+
522
+ async saveBlackboardViewLayout(layout: LayoutRecord): Promise<void> {
523
+ if (!this.db) {
524
+ this.inMemoryStore.get('layout_blackboard_view')?.set(layout.node_id, layout);
525
+ return;
526
+ }
527
+
528
+ try {
529
+ const tx = this.db.transaction('layout_blackboard_view', 'readwrite');
530
+ const store = tx.objectStore('layout_blackboard_view');
531
+ await promisifyRequest(store.put(layout));
532
+ } catch (error) {
533
+ console.error('[IndexedDB] Failed to save blackboard view layout:', error);
534
+ }
535
+ }
536
+
537
+ async getBlackboardViewLayout(nodeId: string): Promise<LayoutRecord | undefined> {
538
+ if (!this.db) {
539
+ return this.inMemoryStore.get('layout_blackboard_view')?.get(nodeId);
540
+ }
541
+
542
+ try {
543
+ const tx = this.db.transaction('layout_blackboard_view', 'readonly');
544
+ const store = tx.objectStore('layout_blackboard_view');
545
+ return await promisifyRequest(store.get(nodeId));
546
+ } catch (error) {
547
+ console.error('[IndexedDB] Failed to get blackboard view layout:', error);
548
+ return undefined;
549
+ }
550
+ }
551
+
552
+ async getAllBlackboardViewLayouts(): Promise<LayoutRecord[]> {
553
+ if (!this.db) {
554
+ const layouts = this.inMemoryStore.get('layout_blackboard_view');
555
+ return layouts ? Array.from(layouts.values()) : [];
556
+ }
557
+
558
+ try {
559
+ const tx = this.db.transaction('layout_blackboard_view', 'readonly');
560
+ const store = tx.objectStore('layout_blackboard_view');
561
+ return await promisifyRequest(store.getAll());
562
+ } catch (error) {
563
+ console.error('[IndexedDB] Failed to get all blackboard view layouts:', error);
564
+ return [];
565
+ }
566
+ }
567
+
568
+ // ============================================================================
569
+ // Module Instance Persistence
570
+ // ============================================================================
571
+
572
+ async saveModuleInstance(instance: ModuleInstanceRecord): Promise<void> {
573
+ if (!this.db) {
574
+ this.inMemoryStore.get('module_instances')?.set(instance.instance_id, instance);
575
+ return;
576
+ }
577
+
578
+ try {
579
+ const tx = this.db.transaction('module_instances', 'readwrite');
580
+ const store = tx.objectStore('module_instances');
581
+ await promisifyRequest(store.put(instance));
582
+ } catch (error) {
583
+ console.error('[IndexedDB] Failed to save module instance:', error);
584
+ }
585
+ }
586
+
587
+ async getModuleInstance(instanceId: string): Promise<ModuleInstanceRecord | undefined> {
588
+ if (!this.db) {
589
+ return this.inMemoryStore.get('module_instances')?.get(instanceId);
590
+ }
591
+
592
+ try {
593
+ const tx = this.db.transaction('module_instances', 'readonly');
594
+ const store = tx.objectStore('module_instances');
595
+ return await promisifyRequest(store.get(instanceId));
596
+ } catch (error) {
597
+ console.error('[IndexedDB] Failed to get module instance:', error);
598
+ return undefined;
599
+ }
600
+ }
601
+
602
+ async getAllModuleInstances(): Promise<ModuleInstanceRecord[]> {
603
+ if (!this.db) {
604
+ const instances = this.inMemoryStore.get('module_instances');
605
+ return instances ? Array.from(instances.values()) : [];
606
+ }
607
+
608
+ try {
609
+ const tx = this.db.transaction('module_instances', 'readonly');
610
+ const store = tx.objectStore('module_instances');
611
+ return await promisifyRequest(store.getAll());
612
+ } catch (error) {
613
+ console.error('[IndexedDB] Failed to get all module instances:', error);
614
+ return [];
615
+ }
616
+ }
617
+
618
+ async deleteModuleInstance(instanceId: string): Promise<void> {
619
+ if (!this.db) {
620
+ this.inMemoryStore.get('module_instances')?.delete(instanceId);
621
+ return;
622
+ }
623
+
624
+ try {
625
+ const tx = this.db.transaction('module_instances', 'readwrite');
626
+ const store = tx.objectStore('module_instances');
627
+ await promisifyRequest(store.delete(instanceId));
628
+ } catch (error) {
629
+ console.error('[IndexedDB] Failed to delete module instance:', error);
630
+ }
631
+ }
632
+
633
+ // ============================================================================
634
+ // Session Management
635
+ // ============================================================================
636
+
637
+ async saveSession(session: SessionRecord): Promise<void> {
638
+ if (!this.db) {
639
+ this.inMemoryStore.get('sessions')?.set(session.session_id, session);
640
+ return;
641
+ }
642
+
643
+ try {
644
+ const tx = this.db.transaction('sessions', 'readwrite');
645
+ const store = tx.objectStore('sessions');
646
+ await promisifyRequest(store.put(session));
647
+ } catch (error) {
648
+ console.error('[IndexedDB] Failed to save session:', error);
649
+ }
650
+ }
651
+
652
+ async getAllSessions(): Promise<SessionRecord[]> {
653
+ if (!this.db) {
654
+ const sessions = this.inMemoryStore.get('sessions');
655
+ return sessions ? Array.from(sessions.values()) : [];
656
+ }
657
+
658
+ try {
659
+ const tx = this.db.transaction('sessions', 'readonly');
660
+ const store = tx.objectStore('sessions');
661
+ return await promisifyRequest(store.getAll());
662
+ } catch (error) {
663
+ console.error('[IndexedDB] Failed to get all sessions:', error);
664
+ return [];
665
+ }
666
+ }
667
+
668
+ // ============================================================================
669
+ // LRU Eviction Strategy
670
+ // ============================================================================
671
+
672
+ /**
673
+ * Check if eviction should be triggered (80% quota threshold)
674
+ */
675
+ async checkShouldEvict(): Promise<boolean> {
676
+ if (!this.db) {
677
+ return false;
678
+ }
679
+
680
+ try {
681
+ if (!navigator.storage?.estimate) {
682
+ return false;
683
+ }
684
+
685
+ const estimate = await navigator.storage.estimate();
686
+ const usage = estimate.usage || 0;
687
+ const quota = estimate.quota || 0;
688
+
689
+ if (quota === 0) {
690
+ return false;
691
+ }
692
+
693
+ const percentage = usage / quota;
694
+
695
+ if (percentage > EVICTION_THRESHOLD) {
696
+ console.warn(
697
+ `[IndexedDB] Storage quota exceeded threshold: ${(percentage * 100).toFixed(1)}% (${usage}/${quota} bytes)`
698
+ );
699
+ return true;
700
+ }
701
+
702
+ return false;
703
+ } catch (error) {
704
+ console.error('[IndexedDB] Failed to check storage quota:', error);
705
+ return false;
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Evict oldest sessions until usage reaches 60% target
711
+ */
712
+ async evictOldSessions(): Promise<void> {
713
+ if (!this.db) {
714
+ return;
715
+ }
716
+
717
+ try {
718
+ // Get all sessions sorted by created_at (oldest first)
719
+ const tx = this.db.transaction('sessions', 'readwrite');
720
+ const store = tx.objectStore('sessions');
721
+ const index = store.index('created_at');
722
+ const sessions = await promisifyRequest(index.getAll());
723
+
724
+ if (sessions.length === 0) {
725
+ return;
726
+ }
727
+
728
+ // Evict sessions one by one until target reached
729
+ for (const session of sessions) {
730
+ const estimate = await navigator.storage.estimate();
731
+ const usage = estimate.usage || 0;
732
+ const quota = estimate.quota || 0;
733
+
734
+ if (quota === 0) break;
735
+
736
+ const percentage = usage / quota;
737
+
738
+ // Stop if we've reached the target
739
+ if (percentage <= EVICTION_TARGET) {
740
+ break;
741
+ }
742
+
743
+ // Delete session
744
+ const deleteTx = this.db.transaction('sessions', 'readwrite');
745
+ const deleteStore = deleteTx.objectStore('sessions');
746
+ await promisifyRequest(deleteStore.delete(session.session_id));
747
+ console.log(`[IndexedDB] Evicted session: ${session.session_id}`);
748
+ }
749
+ } catch (error) {
750
+ console.error('[IndexedDB] Failed to evict old sessions:', error);
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Check and evict by record limit (MAX_RECORDS_PER_STORE)
756
+ * Evicts oldest records based on specified index
757
+ */
758
+ private async checkAndEvictByRecordLimit(
759
+ storeName: string,
760
+ sortIndex: string
761
+ ): Promise<void> {
762
+ if (!this.db) {
763
+ return;
764
+ }
765
+
766
+ try {
767
+ const tx = this.db.transaction(storeName, 'readonly');
768
+ const store = tx.objectStore(storeName);
769
+ const count = await promisifyRequest(store.count());
770
+
771
+ if (count > MAX_RECORDS_PER_STORE) {
772
+ // Get oldest records using the sort index
773
+ const readTx = this.db.transaction(storeName, 'readonly');
774
+ const readStore = readTx.objectStore(storeName);
775
+ const index = readStore.index(sortIndex);
776
+ const keys = await promisifyRequest(index.getAllKeys());
777
+
778
+ // Delete oldest records until we're under the limit
779
+ const numToDelete = count - MAX_RECORDS_PER_STORE;
780
+ const writeTx = this.db.transaction(storeName, 'readwrite');
781
+ const writeStore = writeTx.objectStore(storeName);
782
+
783
+ for (let i = 0; i < numToDelete && i < keys.length; i++) {
784
+ await promisifyRequest(writeStore.delete(keys[i]!));
785
+ }
786
+ }
787
+ } catch (error) {
788
+ console.error(`[IndexedDB] Failed to evict by record limit for ${storeName}:`, error);
789
+ }
790
+ }
791
+ }
792
+
793
+ // Singleton instance
794
+ export const indexedDBService = new IndexedDBService();