@codeguide/core 0.0.27 → 0.0.29
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.
- package/README.md +50 -41
- package/__tests__/services/codespace/codespace-v2.test.ts +29 -18
- package/__tests__/services/usage/usage-service.test.ts +597 -85
- package/codeguide.ts +6 -0
- package/dist/codeguide.d.ts +3 -1
- package/dist/codeguide.js +2 -0
- package/dist/index.d.ts +4 -3
- package/dist/services/base/base-service.d.ts +21 -0
- package/dist/services/base/base-service.js +114 -0
- package/dist/services/codespace/codespace-service.d.ts +55 -1
- package/dist/services/codespace/codespace-service.js +260 -5
- package/dist/services/codespace/codespace-types.d.ts +193 -13
- package/dist/services/codespace/index.d.ts +1 -1
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +7 -1
- package/dist/services/projects/project-types.d.ts +66 -32
- package/dist/services/repository-analysis/repository-types.d.ts +1 -0
- package/dist/services/starter-kits/index.d.ts +2 -0
- package/dist/services/starter-kits/index.js +20 -0
- package/dist/services/starter-kits/starter-kits-service.d.ts +13 -0
- package/dist/services/starter-kits/starter-kits-service.js +27 -0
- package/dist/services/starter-kits/starter-kits-types.d.ts +34 -0
- package/dist/services/starter-kits/starter-kits-types.js +2 -0
- package/dist/services/tasks/task-service.d.ts +2 -1
- package/dist/services/tasks/task-service.js +8 -0
- package/dist/services/tasks/task-types.d.ts +26 -7
- package/dist/services/usage/usage-service.d.ts +5 -2
- package/dist/services/usage/usage-service.js +58 -9
- package/dist/services/usage/usage-types.d.ts +207 -34
- package/dist/services/users/index.d.ts +2 -0
- package/dist/services/users/index.js +20 -0
- package/dist/services/users/user-service.d.ts +12 -0
- package/dist/services/users/user-service.js +17 -0
- package/dist/services/users/user-types.d.ts +55 -0
- package/dist/services/users/user-types.js +2 -0
- package/docs/.vitepress/README.md +51 -0
- package/docs/.vitepress/config.ts +139 -0
- package/docs/.vitepress/theme/custom.css +80 -0
- package/docs/.vitepress/theme/index.ts +13 -0
- package/docs/.vitepress/tsconfig.json +19 -0
- package/docs/QUICKSTART.md +77 -0
- package/docs/README.md +134 -0
- package/docs/README_SETUP.md +46 -0
- package/docs/authentication.md +351 -0
- package/docs/codeguide-client.md +350 -0
- package/docs/codespace-models.md +1004 -0
- package/docs/codespace-service.md +558 -81
- package/docs/index.md +135 -0
- package/docs/package.json +14 -0
- package/docs/projects-service.md +688 -0
- package/docs/security-keys-service.md +773 -0
- package/docs/starter-kits-service.md +249 -0
- package/docs/task-service.md +955 -0
- package/docs/testsprite_tests/TC001_Homepage_Load_and_Hero_Section_Display.py +70 -0
- package/docs/testsprite_tests/TC002_Sidebar_Navigation_ExpandCollapse_Functionality.py +73 -0
- package/docs/testsprite_tests/TC003_Full_Text_Local_Search_with_Keyboard_Shortcut.py +90 -0
- package/docs/testsprite_tests/TC004_Dark_Mode_Toggle_and_Persistence.py +73 -0
- package/docs/testsprite_tests/TC005_Mobile_Responsiveness_and_Touch_Navigation.py +113 -0
- package/docs/testsprite_tests/TC006_GitHub_Integration_Edit_this_page_Links.py +73 -0
- package/docs/testsprite_tests/TC007_Syntax_Highlighting_and_Code_Copy_Functionality.py +73 -0
- package/docs/testsprite_tests/TC008_Auto_Generated_Table_of_Contents_Accuracy.py +73 -0
- package/docs/testsprite_tests/TC009_SEO_and_Content_Discoverability_Verification.py +73 -0
- package/docs/testsprite_tests/TC010_Accessibility_Compliance_WCAG_AA.py +73 -0
- package/docs/testsprite_tests/TC011_Local_Development_Workflow_Build_and_Hot_Reload.py +74 -0
- package/docs/testsprite_tests/TC012_Performance_Metrics_Compliance.py +73 -0
- package/docs/testsprite_tests/standard_prd.json +122 -0
- package/docs/testsprite_tests/testsprite-mcp-test-report.html +2508 -0
- package/docs/testsprite_tests/testsprite-mcp-test-report.md +273 -0
- package/docs/testsprite_tests/testsprite_frontend_test_plan.json +390 -0
- package/docs/usage-service.md +616 -0
- package/index.ts +11 -3
- package/package.json +16 -2
- package/plans/CODESPACE_LOGS_STREAMING_GUIDE.md +320 -0
- package/plans/CODESPACE_TASK_LOGS_API_COMPLETE_GUIDE.md +821 -0
- package/services/base/base-service.ts +130 -0
- package/services/codespace/codespace-service.ts +347 -8
- package/services/codespace/codespace-types.ts +263 -14
- package/services/codespace/index.ts +16 -1
- package/services/index.ts +4 -0
- package/services/projects/README.md +107 -34
- package/services/projects/project-types.ts +69 -32
- package/services/repository-analysis/repository-types.ts +1 -0
- package/services/starter-kits/index.ts +2 -0
- package/services/starter-kits/starter-kits-service.ts +33 -0
- package/services/starter-kits/starter-kits-types.ts +38 -0
- package/services/tasks/task-service.ts +10 -0
- package/services/tasks/task-types.ts +29 -7
- package/services/usage/usage-service.ts +59 -10
- package/services/usage/usage-types.ts +239 -34
- package/services/users/index.ts +2 -0
- package/services/users/user-service.ts +15 -0
- package/services/users/user-types.ts +59 -0
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
# Codespace Task Logs API - Complete Implementation Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
This guide provides complete documentation for the codespace task logs API implementation, including response types, usage examples, and integration details.
|
|
5
|
+
|
|
6
|
+
## 📊 Database Schema
|
|
7
|
+
|
|
8
|
+
```sql
|
|
9
|
+
CREATE TABLE public.codespace_task_logs (
|
|
10
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
11
|
+
codespace_task_id uuid NOT NULL,
|
|
12
|
+
step_name text NOT NULL,
|
|
13
|
+
log_type text NOT NULL DEFAULT 'info'::text,
|
|
14
|
+
message text NOT NULL,
|
|
15
|
+
created_at timestamp with time zone NULL DEFAULT now(),
|
|
16
|
+
progress_percentage integer NULL,
|
|
17
|
+
metadata jsonb NULL,
|
|
18
|
+
CONSTRAINT codespace_task_logs_pkey PRIMARY KEY (id),
|
|
19
|
+
CONSTRAINT codespace_task_logs_codespace_task_id_fkey
|
|
20
|
+
FOREIGN KEY (codespace_task_id) REFERENCES codespace_tasks (id) ON DELETE CASCADE,
|
|
21
|
+
CONSTRAINT codespace_task_logs_progress_percentage_check
|
|
22
|
+
CHECK ((progress_percentage >= 0 AND progress_percentage <= 100)),
|
|
23
|
+
CONSTRAINT codespace_task_logs_type_check
|
|
24
|
+
CHECK ((log_type = ANY (ARRAY['thinking'::text, 'coding'::text, 'info'::text, 'error'::text, 'success'::text])))
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- Indexes for performance
|
|
28
|
+
CREATE INDEX idx_codespace_task_logs_task_id ON public.codespace_task_logs USING btree (codespace_task_id);
|
|
29
|
+
CREATE INDEX idx_codespace_task_logs_task_id_created ON public.codespace_task_logs USING btree (codespace_task_id, created_at);
|
|
30
|
+
CREATE INDEX idx_codespace_task_logs_log_type ON public.codespace_task_logs USING btree (log_type);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 🏗️ Data Models
|
|
34
|
+
|
|
35
|
+
### 1. Database Model
|
|
36
|
+
```python
|
|
37
|
+
class CodespaceTaskLogInDB(BaseModel):
|
|
38
|
+
"""Codespace task log database model."""
|
|
39
|
+
|
|
40
|
+
id: str
|
|
41
|
+
codespace_task_id: str
|
|
42
|
+
step_name: str
|
|
43
|
+
log_type: Literal["thinking", "coding", "info", "error", "success"]
|
|
44
|
+
message: str
|
|
45
|
+
created_at: datetime
|
|
46
|
+
progress_percentage: Optional[int] = None
|
|
47
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
48
|
+
|
|
49
|
+
model_config = {"from_attributes": True}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Response Model
|
|
53
|
+
```python
|
|
54
|
+
class CodespaceTaskLogResponse(BaseModel):
|
|
55
|
+
"""Response model for a single codespace task log."""
|
|
56
|
+
|
|
57
|
+
id: str
|
|
58
|
+
step_name: str
|
|
59
|
+
log_type: str
|
|
60
|
+
message: str
|
|
61
|
+
created_at: datetime
|
|
62
|
+
progress_percentage: Optional[int] = None
|
|
63
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Paginated List Response
|
|
67
|
+
```python
|
|
68
|
+
class CodespaceTaskLogsListResponse(BaseModel):
|
|
69
|
+
"""Response model for getting a list of codespace task logs."""
|
|
70
|
+
|
|
71
|
+
status: str = "success"
|
|
72
|
+
data: List[CodespaceTaskLogResponse]
|
|
73
|
+
total_count: int = Field(..., description="Total number of logs matching the criteria")
|
|
74
|
+
has_more: bool = Field(..., description="Whether there are more logs available")
|
|
75
|
+
next_offset: Optional[int] = Field(None, description="Next offset to use for pagination")
|
|
76
|
+
message: str = "Codespace task logs retrieved successfully"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 🚀 API Endpoints
|
|
80
|
+
|
|
81
|
+
### 1. Standard REST API Endpoint
|
|
82
|
+
|
|
83
|
+
**GET `/codespace/task/{codespace_task_id}/logs`**
|
|
84
|
+
|
|
85
|
+
#### Query Parameters
|
|
86
|
+
| Parameter | Type | Default | Constraints | Description |
|
|
87
|
+
|-----------|------|---------|-------------|-------------|
|
|
88
|
+
| `limit` | int | 50 | 1-500 | Number of logs to return (custom rows) |
|
|
89
|
+
| `offset` | int | 0 | ≥0 | Number of logs to skip for pagination |
|
|
90
|
+
| `log_type` | string | optional | thinking,coding,info,error,success | Filter by log type |
|
|
91
|
+
| `step_name` | string | optional | text | Filter by step name (partial match) |
|
|
92
|
+
| `search` | string | optional | text | Search in message content |
|
|
93
|
+
| `sort_by` | string | created_at | created_at,step_name,log_type | Sort field |
|
|
94
|
+
| `sort_order` | string | desc | asc,desc | Sort order |
|
|
95
|
+
| `since` | string | optional | ISO timestamp | Get logs after timestamp |
|
|
96
|
+
|
|
97
|
+
#### Response Example
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"status": "success",
|
|
101
|
+
"data": [
|
|
102
|
+
{
|
|
103
|
+
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
104
|
+
"step_name": "task_creation",
|
|
105
|
+
"log_type": "info",
|
|
106
|
+
"message": "🚀 Your coding task is ready!",
|
|
107
|
+
"created_at": "2024-01-15T10:30:00.000Z",
|
|
108
|
+
"progress_percentage": 10,
|
|
109
|
+
"metadata": null
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "123e4567-e89b-12d3-a456-426614174001",
|
|
113
|
+
"step_name": "ai_summary",
|
|
114
|
+
"log_type": "info",
|
|
115
|
+
"message": "🔍 Preparing project foundation (description + outline)",
|
|
116
|
+
"created_at": "2024-01-15T10:30:15.000Z",
|
|
117
|
+
"progress_percentage": 25,
|
|
118
|
+
"metadata": null
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"total_count": 150,
|
|
122
|
+
"has_more": true,
|
|
123
|
+
"next_offset": 50,
|
|
124
|
+
"message": "Retrieved 50 logs"
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 2. Real-time Streaming Endpoint (SSE)
|
|
129
|
+
|
|
130
|
+
**GET `/codespace/task/{codespace_task_id}/logs/stream`**
|
|
131
|
+
|
|
132
|
+
#### Query Parameters
|
|
133
|
+
| Parameter | Type | Default | Constraints | Description |
|
|
134
|
+
|-----------|------|---------|-------------|-------------|
|
|
135
|
+
| `since` | string | optional | ISO timestamp | Start from this timestamp |
|
|
136
|
+
| `timeout` | int | 300 | 30-1800 | Stream timeout in seconds |
|
|
137
|
+
|
|
138
|
+
#### SSE Events
|
|
139
|
+
|
|
140
|
+
**`log` Event**
|
|
141
|
+
```
|
|
142
|
+
event: log
|
|
143
|
+
data: {
|
|
144
|
+
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
145
|
+
"step_name": "task_creation",
|
|
146
|
+
"log_type": "info",
|
|
147
|
+
"message": "🚀 Your coding task is ready!",
|
|
148
|
+
"created_at": "2024-01-15T10:30:00.000Z",
|
|
149
|
+
"progress_percentage": 10,
|
|
150
|
+
"metadata": {}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**`heartbeat` Event**
|
|
155
|
+
```
|
|
156
|
+
event: heartbeat
|
|
157
|
+
data: {
|
|
158
|
+
"timestamp": "2024-01-15T10:30:30.000Z"
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**`complete` Event**
|
|
163
|
+
```
|
|
164
|
+
event: complete
|
|
165
|
+
data: {
|
|
166
|
+
"message": "Task completed"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**`timeout` Event**
|
|
171
|
+
```
|
|
172
|
+
event: timeout
|
|
173
|
+
data: {
|
|
174
|
+
"message": "Stream timeout reached"
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**`error` Event**
|
|
179
|
+
```
|
|
180
|
+
event: error
|
|
181
|
+
data: {
|
|
182
|
+
"error": "Task not found or access denied"
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 💻 Implementation Guide
|
|
187
|
+
|
|
188
|
+
### Frontend Integration Examples
|
|
189
|
+
|
|
190
|
+
#### React Hook (Recommended)
|
|
191
|
+
```typescript
|
|
192
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
193
|
+
|
|
194
|
+
interface CodespaceLog {
|
|
195
|
+
id: string;
|
|
196
|
+
step_name: string;
|
|
197
|
+
log_type: 'thinking' | 'coding' | 'info' | 'error' | 'success';
|
|
198
|
+
message: string;
|
|
199
|
+
created_at: string;
|
|
200
|
+
progress_percentage?: number;
|
|
201
|
+
metadata?: any;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface UseCodespaceLogsReturn {
|
|
205
|
+
logs: CodespaceLog[];
|
|
206
|
+
isLoading: boolean;
|
|
207
|
+
isStreaming: boolean;
|
|
208
|
+
error: string | null;
|
|
209
|
+
hasMore: boolean;
|
|
210
|
+
nextOffset: number | null;
|
|
211
|
+
loadMore: () => void;
|
|
212
|
+
startStreaming: () => void;
|
|
213
|
+
stopStreaming: () => void;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export const useCodespaceLogs = (
|
|
217
|
+
taskId: string,
|
|
218
|
+
authToken: string,
|
|
219
|
+
options: {
|
|
220
|
+
pageSize?: number;
|
|
221
|
+
enableStreaming?: boolean;
|
|
222
|
+
filters?: {
|
|
223
|
+
log_type?: string;
|
|
224
|
+
step_name?: string;
|
|
225
|
+
search?: string;
|
|
226
|
+
};
|
|
227
|
+
} = {}
|
|
228
|
+
): UseCodespaceLogsReturn => {
|
|
229
|
+
const [logs, setLogs] = useState<CodespaceLog[]>([]);
|
|
230
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
231
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
232
|
+
const [error, setError] = useState<string | null>(null);
|
|
233
|
+
const [hasMore, setHasMore] = useState(false);
|
|
234
|
+
const [nextOffset, setNextOffset] = useState<number | null>(null);
|
|
235
|
+
const [eventSource, setEventSource] = useState<EventSource | null>(null);
|
|
236
|
+
|
|
237
|
+
const { pageSize = 50, enableStreaming = true, filters = {} } = options;
|
|
238
|
+
|
|
239
|
+
// Load initial logs
|
|
240
|
+
const loadLogs = useCallback(async (offset = 0) => {
|
|
241
|
+
try {
|
|
242
|
+
setIsLoading(true);
|
|
243
|
+
setError(null);
|
|
244
|
+
|
|
245
|
+
const params = new URLSearchParams({
|
|
246
|
+
limit: pageSize.toString(),
|
|
247
|
+
offset: offset.toString(),
|
|
248
|
+
...filters
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const response = await fetch(
|
|
252
|
+
`/codespace/task/${taskId}/logs?${params}`,
|
|
253
|
+
{
|
|
254
|
+
headers: {
|
|
255
|
+
'Authorization': `Bearer ${authToken}`,
|
|
256
|
+
'Content-Type': 'application/json'
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const data: CodespaceTaskLogsListResponse = await response.json();
|
|
266
|
+
|
|
267
|
+
if (offset === 0) {
|
|
268
|
+
setLogs(data.data);
|
|
269
|
+
} else {
|
|
270
|
+
setLogs(prev => [...prev, ...data.data]);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
setHasMore(data.has_more);
|
|
274
|
+
setNextOffset(data.next_offset);
|
|
275
|
+
|
|
276
|
+
} catch (err) {
|
|
277
|
+
setError(err instanceof Error ? err.message : 'Failed to load logs');
|
|
278
|
+
} finally {
|
|
279
|
+
setIsLoading(false);
|
|
280
|
+
}
|
|
281
|
+
}, [taskId, authToken, pageSize, filters]);
|
|
282
|
+
|
|
283
|
+
// Start streaming
|
|
284
|
+
const startStreaming = useCallback(() => {
|
|
285
|
+
if (!enableStreaming || eventSource) return;
|
|
286
|
+
|
|
287
|
+
const url = `/codespace/task/${taskId}/logs/stream`;
|
|
288
|
+
const source = new EventSource(url);
|
|
289
|
+
|
|
290
|
+
source.addEventListener('log', (event) => {
|
|
291
|
+
try {
|
|
292
|
+
const log: CodespaceLog = JSON.parse(event.data);
|
|
293
|
+
setLogs(prev => [...prev, log]);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
console.error('Failed to parse log data:', err);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
source.addEventListener('complete', () => {
|
|
300
|
+
setIsStreaming(false);
|
|
301
|
+
source.close();
|
|
302
|
+
setEventSource(null);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
source.addEventListener('error', () => {
|
|
306
|
+
setError('Stream error occurred');
|
|
307
|
+
setIsStreaming(false);
|
|
308
|
+
source.close();
|
|
309
|
+
setEventSource(null);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
source.addEventListener('timeout', () => {
|
|
313
|
+
setError('Stream timeout');
|
|
314
|
+
setIsStreaming(false);
|
|
315
|
+
source.close();
|
|
316
|
+
setEventSource(null);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
setEventSource(source);
|
|
320
|
+
setIsStreaming(true);
|
|
321
|
+
}, [taskId, enableStreaming, eventSource]);
|
|
322
|
+
|
|
323
|
+
// Stop streaming
|
|
324
|
+
const stopStreaming = useCallback(() => {
|
|
325
|
+
if (eventSource) {
|
|
326
|
+
eventSource.close();
|
|
327
|
+
setEventSource(null);
|
|
328
|
+
}
|
|
329
|
+
setIsStreaming(false);
|
|
330
|
+
}, [eventSource]);
|
|
331
|
+
|
|
332
|
+
// Load more logs
|
|
333
|
+
const loadMore = useCallback(() => {
|
|
334
|
+
if (nextOffset !== null && !isLoading) {
|
|
335
|
+
loadLogs(nextOffset);
|
|
336
|
+
}
|
|
337
|
+
}, [nextOffset, isLoading, loadLogs]);
|
|
338
|
+
|
|
339
|
+
// Initial load
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
if (taskId) {
|
|
342
|
+
loadLogs(0);
|
|
343
|
+
}
|
|
344
|
+
}, [taskId, loadLogs]);
|
|
345
|
+
|
|
346
|
+
// Auto-start streaming
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
if (enableStreaming && taskId && !eventSource && !isStreaming) {
|
|
349
|
+
startStreaming();
|
|
350
|
+
}
|
|
351
|
+
}, [enableStreaming, taskId, eventSource, isStreaming, startStreaming]);
|
|
352
|
+
|
|
353
|
+
// Cleanup
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
return () => {
|
|
356
|
+
stopStreaming();
|
|
357
|
+
};
|
|
358
|
+
}, [stopStreaming]);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
logs,
|
|
362
|
+
isLoading,
|
|
363
|
+
isStreaming,
|
|
364
|
+
error,
|
|
365
|
+
hasMore,
|
|
366
|
+
nextOffset,
|
|
367
|
+
loadMore,
|
|
368
|
+
startStreaming,
|
|
369
|
+
stopStreaming
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### React Component Example
|
|
375
|
+
```typescript
|
|
376
|
+
import React from 'react';
|
|
377
|
+
import { useCodespaceLogs } from './useCodespaceLogs';
|
|
378
|
+
|
|
379
|
+
interface LogsViewerProps {
|
|
380
|
+
taskId: string;
|
|
381
|
+
authToken: string;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export const LogsViewer: React.FC<LogsViewerProps> = ({ taskId, authToken }) => {
|
|
385
|
+
const {
|
|
386
|
+
logs,
|
|
387
|
+
isLoading,
|
|
388
|
+
isStreaming,
|
|
389
|
+
error,
|
|
390
|
+
hasMore,
|
|
391
|
+
loadMore,
|
|
392
|
+
stopStreaming
|
|
393
|
+
} = useCodespaceLogs(taskId, authToken, {
|
|
394
|
+
pageSize: 100,
|
|
395
|
+
enableStreaming: true,
|
|
396
|
+
filters: {
|
|
397
|
+
// log_type: 'error', // Uncomment to filter errors only
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const getLogIcon = (logType: string) => {
|
|
402
|
+
switch (logType) {
|
|
403
|
+
case 'thinking': return '🤔';
|
|
404
|
+
case 'coding': return '💻';
|
|
405
|
+
case 'info': return 'ℹ️';
|
|
406
|
+
case 'error': return '❌';
|
|
407
|
+
case 'success': return '✅';
|
|
408
|
+
default: return '📝';
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const getLogColor = (logType: string) => {
|
|
413
|
+
switch (logType) {
|
|
414
|
+
case 'thinking': return 'text-blue-600';
|
|
415
|
+
case 'coding': return 'text-green-600';
|
|
416
|
+
case 'info': return 'text-gray-600';
|
|
417
|
+
case 'error': return 'text-red-600';
|
|
418
|
+
case 'success': return 'text-green-500';
|
|
419
|
+
default: return 'text-gray-600';
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
if (error) {
|
|
424
|
+
return (
|
|
425
|
+
<div className="bg-red-50 border border-red-200 rounded-md p-4">
|
|
426
|
+
<div className="text-red-800">Error: {error}</div>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div className="flex flex-col h-full">
|
|
433
|
+
{/* Header */}
|
|
434
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
435
|
+
<h3 className="text-lg font-semibold">Task Logs</h3>
|
|
436
|
+
<div className="flex items-center space-x-4">
|
|
437
|
+
<span className="text-sm text-gray-500">
|
|
438
|
+
{logs.length} logs
|
|
439
|
+
</span>
|
|
440
|
+
<span className={`text-sm ${isStreaming ? 'text-green-600' : 'text-gray-500'}`}>
|
|
441
|
+
{isStreaming ? '🔄 Streaming...' : '⏸️ Paused'}
|
|
442
|
+
</span>
|
|
443
|
+
{isStreaming && (
|
|
444
|
+
<button
|
|
445
|
+
onClick={stopStreaming}
|
|
446
|
+
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 rounded"
|
|
447
|
+
>
|
|
448
|
+
Stop
|
|
449
|
+
</button>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
{/* Logs */}
|
|
455
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-2">
|
|
456
|
+
{isLoading && logs.length === 0 && (
|
|
457
|
+
<div className="text-center text-gray-500 py-8">
|
|
458
|
+
Loading logs...
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
{logs.map((log, index) => (
|
|
463
|
+
<div
|
|
464
|
+
key={log.id || index}
|
|
465
|
+
className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg"
|
|
466
|
+
>
|
|
467
|
+
<span className="text-lg">{getLogIcon(log.log_type)}</span>
|
|
468
|
+
<div className="flex-1 min-w-0">
|
|
469
|
+
<div className="flex items-center space-x-2">
|
|
470
|
+
<span className={`text-sm font-medium ${getLogColor(log.log_type)}`}>
|
|
471
|
+
{log.step_name}
|
|
472
|
+
</span>
|
|
473
|
+
<span className="text-xs text-gray-400">
|
|
474
|
+
{new Date(log.created_at).toLocaleTimeString()}
|
|
475
|
+
</span>
|
|
476
|
+
{log.progress_percentage !== null && (
|
|
477
|
+
<span className="text-xs text-gray-500">
|
|
478
|
+
{log.progress_percentage}%
|
|
479
|
+
</span>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
<div className="text-sm text-gray-700 mt-1 break-words">
|
|
483
|
+
{log.message}
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
))}
|
|
488
|
+
|
|
489
|
+
{hasMore && (
|
|
490
|
+
<div className="text-center py-4">
|
|
491
|
+
<button
|
|
492
|
+
onClick={loadMore}
|
|
493
|
+
disabled={isLoading}
|
|
494
|
+
className="px-4 py-2 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 disabled:opacity-50"
|
|
495
|
+
>
|
|
496
|
+
{isLoading ? 'Loading...' : 'Load More'}
|
|
497
|
+
</button>
|
|
498
|
+
</div>
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
);
|
|
503
|
+
};
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
#### Vue.js Example
|
|
507
|
+
```typescript
|
|
508
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
509
|
+
|
|
510
|
+
export function useCodespaceLogs(taskId: string, authToken: string) {
|
|
511
|
+
const logs = ref<CodespaceLog[]>([]);
|
|
512
|
+
const isStreaming = ref(false);
|
|
513
|
+
const error = ref<string | null>(null);
|
|
514
|
+
let eventSource: EventSource | null = null;
|
|
515
|
+
|
|
516
|
+
const startStreaming = () => {
|
|
517
|
+
if (eventSource) return;
|
|
518
|
+
|
|
519
|
+
eventSource = new EventSource(`/codespace/task/${taskId}/logs/stream`);
|
|
520
|
+
|
|
521
|
+
eventSource.addEventListener('log', (event) => {
|
|
522
|
+
const log = JSON.parse(event.data);
|
|
523
|
+
logs.value.push(log);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
eventSource.addEventListener('complete', () => {
|
|
527
|
+
isStreaming.value = false;
|
|
528
|
+
eventSource?.close();
|
|
529
|
+
eventSource = null;
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
eventSource.addEventListener('error', () => {
|
|
533
|
+
error.value = 'Stream error occurred';
|
|
534
|
+
isStreaming.value = false;
|
|
535
|
+
eventSource?.close();
|
|
536
|
+
eventSource = null;
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
isStreaming.value = true;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const stopStreaming = () => {
|
|
543
|
+
if (eventSource) {
|
|
544
|
+
eventSource.close();
|
|
545
|
+
eventSource = null;
|
|
546
|
+
}
|
|
547
|
+
isStreaming.value = false;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
onMounted(() => {
|
|
551
|
+
startStreaming();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
onUnmounted(() => {
|
|
555
|
+
stopStreaming();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
logs,
|
|
560
|
+
isStreaming,
|
|
561
|
+
error,
|
|
562
|
+
startStreaming,
|
|
563
|
+
stopStreaming
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Backend Integration
|
|
569
|
+
|
|
570
|
+
#### Service Method Usage
|
|
571
|
+
```python
|
|
572
|
+
from app.services.codespace_service import CodespaceService
|
|
573
|
+
|
|
574
|
+
async def get_logs_example():
|
|
575
|
+
"""Example of using the logs service method."""
|
|
576
|
+
service = CodespaceService()
|
|
577
|
+
|
|
578
|
+
try:
|
|
579
|
+
# Get paginated logs
|
|
580
|
+
logs, total_count, has_more = await service.get_codespace_task_logs(
|
|
581
|
+
codespace_task_id="task-uuid",
|
|
582
|
+
user_id="user-uuid",
|
|
583
|
+
limit=100,
|
|
584
|
+
offset=0,
|
|
585
|
+
log_type="error",
|
|
586
|
+
search="failed",
|
|
587
|
+
sort_by="created_at",
|
|
588
|
+
sort_order="desc"
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
"logs": logs,
|
|
593
|
+
"total_count": total_count,
|
|
594
|
+
"has_more": has_more
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
except Exception as e:
|
|
598
|
+
logger.error(f"Failed to get logs: {e}")
|
|
599
|
+
raise
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## 🔧 Configuration & Deployment
|
|
603
|
+
|
|
604
|
+
### Environment Variables
|
|
605
|
+
```bash
|
|
606
|
+
# Required for streaming
|
|
607
|
+
APP_BASE_URL=https://your-app.com
|
|
608
|
+
CLERK_SECRET_KEY=your-clerk-secret-key
|
|
609
|
+
SUPABASE_URL=your-supabase-url
|
|
610
|
+
SUPABASE_KEY=your-supabase-key
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Nginx Configuration (for streaming)
|
|
614
|
+
```nginx
|
|
615
|
+
location /codespace/task/*/logs/stream {
|
|
616
|
+
proxy_pass http://backend;
|
|
617
|
+
proxy_set_header Host $host;
|
|
618
|
+
proxy_set_header Connection '';
|
|
619
|
+
proxy_http_version 1.1;
|
|
620
|
+
proxy_set_header X-Accel-Buffering no;
|
|
621
|
+
proxy_cache off;
|
|
622
|
+
proxy_set_header Proxy '';
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Database Indexes (Performance)
|
|
627
|
+
```sql
|
|
628
|
+
-- Create indexes for optimal performance
|
|
629
|
+
CREATE INDEX CONCURRENTLY idx_codespace_logs_composite
|
|
630
|
+
ON codespace_task_logs (codespace_task_id, created_at DESC);
|
|
631
|
+
|
|
632
|
+
CREATE INDEX CONCURRENTLY idx_codespace_logs_type_task
|
|
633
|
+
ON codespace_task_logs (log_type, codespace_task_id);
|
|
634
|
+
|
|
635
|
+
CREATE INDEX CONCURRENTLY idx_codespace_logs_step_task
|
|
636
|
+
ON codespace_task_logs (step_name, codespace_task_id);
|
|
637
|
+
|
|
638
|
+
-- For text search (if using PostgreSQL)
|
|
639
|
+
CREATE INDEX CONCURRENTLY idx_codespace_logs_message_gin
|
|
640
|
+
ON codespace_task_logs USING gin(to_tsvector('english', message));
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
## 🧪 Testing
|
|
644
|
+
|
|
645
|
+
### Unit Tests
|
|
646
|
+
```python
|
|
647
|
+
import pytest
|
|
648
|
+
from app.services.codespace_service import CodespaceService
|
|
649
|
+
|
|
650
|
+
@pytest.mark.asyncio
|
|
651
|
+
async def test_get_codespace_task_logs():
|
|
652
|
+
"""Test the logs retrieval method."""
|
|
653
|
+
service = CodespaceService()
|
|
654
|
+
|
|
655
|
+
logs, total_count, has_more = await service.get_codespace_task_logs(
|
|
656
|
+
codespace_task_id="test-task-id",
|
|
657
|
+
user_id="test-user-id",
|
|
658
|
+
limit=10,
|
|
659
|
+
offset=0
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
assert isinstance(logs, list)
|
|
663
|
+
assert isinstance(total_count, int)
|
|
664
|
+
assert isinstance(has_more, bool)
|
|
665
|
+
assert len(logs) <= 10
|
|
666
|
+
|
|
667
|
+
@pytest.mark.asyncio
|
|
668
|
+
async def test_stream_codespace_task_logs():
|
|
669
|
+
"""Test the streaming method."""
|
|
670
|
+
service = CodespaceService()
|
|
671
|
+
|
|
672
|
+
log_stream = service.stream_codespace_task_logs(
|
|
673
|
+
codespace_task_id="test-task-id",
|
|
674
|
+
user_id="test-user-id",
|
|
675
|
+
timeout_seconds=10
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
events = []
|
|
679
|
+
async for event in log_stream:
|
|
680
|
+
events.append(event)
|
|
681
|
+
if event.startswith('event: complete'):
|
|
682
|
+
break
|
|
683
|
+
|
|
684
|
+
assert len(events) > 0
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### Integration Tests
|
|
688
|
+
```python
|
|
689
|
+
import pytest
|
|
690
|
+
from fastapi.testclient import TestClient
|
|
691
|
+
from app.main import app
|
|
692
|
+
|
|
693
|
+
client = TestClient(app)
|
|
694
|
+
|
|
695
|
+
def test_get_logs_endpoint():
|
|
696
|
+
"""Test the REST API endpoint."""
|
|
697
|
+
response = client.get(
|
|
698
|
+
"/codespace/task/test-task-id/logs",
|
|
699
|
+
headers={"Authorization": "Bearer test-token"},
|
|
700
|
+
params={"limit": 10, "log_type": "info"}
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
assert response.status_code == 200
|
|
704
|
+
data = response.json()
|
|
705
|
+
assert "data" in data
|
|
706
|
+
assert "total_count" in data
|
|
707
|
+
assert "has_more" in data
|
|
708
|
+
assert len(data["data"]) <= 10
|
|
709
|
+
|
|
710
|
+
def test_stream_logs_endpoint():
|
|
711
|
+
"""Test the streaming endpoint."""
|
|
712
|
+
with client.stream(
|
|
713
|
+
"GET",
|
|
714
|
+
"/codespace/task/test-task-id/logs/stream",
|
|
715
|
+
headers={"Authorization": "Bearer test-token"}
|
|
716
|
+
) as response:
|
|
717
|
+
assert response.status_code == 200
|
|
718
|
+
assert response.headers["content-type"] == "text/event-stream; charset=utf-8"
|
|
719
|
+
|
|
720
|
+
# Read first few events
|
|
721
|
+
events = []
|
|
722
|
+
for line in response.iter_lines():
|
|
723
|
+
if line:
|
|
724
|
+
events.append(line.decode('utf-8'))
|
|
725
|
+
if len(events) >= 5:
|
|
726
|
+
break
|
|
727
|
+
|
|
728
|
+
assert len(events) > 0
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
## 📈 Performance Considerations
|
|
732
|
+
|
|
733
|
+
### Pagination Strategy
|
|
734
|
+
- Use `limit` parameter to control memory usage
|
|
735
|
+
- Implement client-side caching for recent logs
|
|
736
|
+
- Consider virtual scrolling for large log lists
|
|
737
|
+
|
|
738
|
+
### Streaming Optimization
|
|
739
|
+
- Set appropriate timeout values (5-30 minutes)
|
|
740
|
+
- Implement client-side reconnection logic
|
|
741
|
+
- Monitor concurrent connection limits
|
|
742
|
+
|
|
743
|
+
### Database Optimization
|
|
744
|
+
- Use composite indexes for common query patterns
|
|
745
|
+
- Consider log archiving for old completed tasks
|
|
746
|
+
- Implement database connection pooling
|
|
747
|
+
|
|
748
|
+
### Caching Strategy
|
|
749
|
+
```python
|
|
750
|
+
# Redis caching example
|
|
751
|
+
import redis
|
|
752
|
+
|
|
753
|
+
redis_client = redis.Redis(host='localhost', port=6379, db=0)
|
|
754
|
+
|
|
755
|
+
async def get_cached_logs(task_id: str, user_id: str, limit: int):
|
|
756
|
+
cache_key = f"logs:{task_id}:{user_id}:{limit}"
|
|
757
|
+
cached = redis_client.get(cache_key)
|
|
758
|
+
|
|
759
|
+
if cached:
|
|
760
|
+
return json.loads(cached)
|
|
761
|
+
|
|
762
|
+
# Fetch from database
|
|
763
|
+
logs, total_count, has_more = await service.get_codespace_task_logs(...)
|
|
764
|
+
|
|
765
|
+
# Cache for 30 seconds
|
|
766
|
+
redis_client.setex(cache_key, 30, json.dumps({
|
|
767
|
+
'logs': logs,
|
|
768
|
+
'total_count': total_count,
|
|
769
|
+
'has_more': has_more
|
|
770
|
+
}))
|
|
771
|
+
|
|
772
|
+
return logs, total_count, has_more
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
## 🔍 Troubleshooting
|
|
776
|
+
|
|
777
|
+
### Common Issues
|
|
778
|
+
|
|
779
|
+
1. **SSE Connection Closes Immediately**
|
|
780
|
+
- Check authentication token
|
|
781
|
+
- Verify user has permission to access the task
|
|
782
|
+
- Check CORS headers
|
|
783
|
+
|
|
784
|
+
2. **No Logs Received**
|
|
785
|
+
- Verify task ID exists
|
|
786
|
+
- Check if task has generated any logs yet
|
|
787
|
+
- Ensure database connection is working
|
|
788
|
+
|
|
789
|
+
3. **Performance Issues**
|
|
790
|
+
- Add database indexes
|
|
791
|
+
- Reduce `limit` parameter
|
|
792
|
+
- Implement caching
|
|
793
|
+
|
|
794
|
+
4. **Memory Issues**
|
|
795
|
+
- Implement log pagination on client
|
|
796
|
+
- Use virtual scrolling for UI
|
|
797
|
+
- Consider log archiving
|
|
798
|
+
|
|
799
|
+
### Debug Mode
|
|
800
|
+
```python
|
|
801
|
+
# Enable debug logging
|
|
802
|
+
import logging
|
|
803
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
804
|
+
|
|
805
|
+
# Add debug headers to streaming response
|
|
806
|
+
headers = {
|
|
807
|
+
"Cache-Control": "no-cache",
|
|
808
|
+
"Connection": "keep-alive",
|
|
809
|
+
"Access-Control-Allow-Origin": "*",
|
|
810
|
+
"Access-Control-Allow-Headers": "Cache-Control",
|
|
811
|
+
"X-Accel-Buffering": "no",
|
|
812
|
+
"X-Debug-Mode": "true" # Enable debug information
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
## 📚 Additional Resources
|
|
817
|
+
|
|
818
|
+
- [Server-Sent Events MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|
|
819
|
+
- [FastAPI StreamingResponse](https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse)
|
|
820
|
+
- [PostgreSQL Index Optimization](https://www.postgresql.org/docs/current/indexes.html)
|
|
821
|
+
- [Real-time Web Applications](https://ably.com/topic/realtime-web-applications)
|